From f6f54c3af3c174bbad2fbe731f543d3db9203c6d Mon Sep 17 00:00:00 2001 From: anton Date: Thu, 29 Jan 2026 16:28:21 +0800 Subject: [PATCH 1/9] feat: separate official trending from custom tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Official Trending (backend-computed): - showOfficialTrending setting (default ON) - 🔥 icon in market indicators - Filter: 'Trending Only' uses backend isTrending Custom Tags (user-defined): - customTagConfig with icon + thresholds - 20 react-icons to choose from - Filter: 'Custom Tag Only' - Configure via settings panel Changes: - useMarketPreferences: add showOfficialTrending, rename to customTagConfig - custom-tag-icons.tsx: icon picker component with 20 icons - useMarketMetricsQuery: add isTrending/trendingReason types, separate hooks - useFilteredMarkets: handle both filter modes - market-indicators: show both icons when applicable - ExperimentalPanel: separate sections for each feature - markets-filter-compact: two filter toggles --- src/components/shared/custom-tag-icons.tsx | 88 +++++++++++++++++ .../markets/components/market-indicators.tsx | 41 ++++++-- .../components/markets-filter-compact.tsx | 34 +++++-- src/hooks/queries/useMarketMetricsQuery.ts | 81 +++++++++------- src/hooks/useFilteredMarkets.ts | 20 +++- .../settings/monarch-settings/constants.ts | 2 +- .../details/TrendingDetail.tsx | 37 +++++--- .../panels/ExperimentalPanel.tsx | 33 +++++-- src/stores/useMarketPreferences.ts | 94 +++++++++++++++---- src/stores/useMarketsFilters.ts | 6 +- 10 files changed, 342 insertions(+), 94 deletions(-) create mode 100644 src/components/shared/custom-tag-icons.tsx diff --git a/src/components/shared/custom-tag-icons.tsx b/src/components/shared/custom-tag-icons.tsx new file mode 100644 index 00000000..b90d3dac --- /dev/null +++ b/src/components/shared/custom-tag-icons.tsx @@ -0,0 +1,88 @@ +'use client'; + +import type { IconType } from 'react-icons'; +import { AiOutlineFire, AiOutlineRocket, AiOutlineStar, AiOutlineThunderbolt, AiOutlineEye, AiOutlineHeart, AiOutlineTrophy, AiOutlineCrown } from 'react-icons/ai'; +import { BiTrendingUp, BiTargetLock, BiBookmark, BiFlag } from 'react-icons/bi'; +import { FaGem, FaCoins, FaBolt, FaChartLine } from 'react-icons/fa'; +import { HiOutlineSparkles, HiOutlineLightningBolt } from 'react-icons/hi'; +import { IoFlameOutline, IoDiamondOutline } from 'react-icons/io5'; +import type { CustomTagIconId } from '@/stores/useMarketPreferences'; + +/** + * Mapping of icon IDs to react-icons components. + * Keep this in sync with CUSTOM_TAG_ICONS in useMarketPreferences. + */ +export const ICON_MAP: Record = { + fire: AiOutlineFire, + rocket: AiOutlineRocket, + star: AiOutlineStar, + bolt: AiOutlineThunderbolt, + gem: FaGem, + chart: FaChartLine, + target: BiTargetLock, + eye: AiOutlineEye, + bookmark: BiBookmark, + flag: BiFlag, + heart: AiOutlineHeart, + coins: FaCoins, + trophy: AiOutlineTrophy, + zap: FaBolt, + trending: BiTrendingUp, + sparkles: HiOutlineSparkles, + flame: IoFlameOutline, + diamond: IoDiamondOutline, + crown: AiOutlineCrown, + lightning: HiOutlineLightningBolt, +}; + +type CustomTagIconProps = { + iconId: CustomTagIconId; + size?: number; + className?: string; +}; + +/** + * Render a custom tag icon by its ID. + */ +export function CustomTagIcon({ iconId, size = 14, className = '' }: CustomTagIconProps) { + const IconComponent = ICON_MAP[iconId]; + if (!IconComponent) return null; + return ; +} + +/** + * Icon picker component for selecting custom tag icons. + */ +type IconPickerProps = { + selectedIcon: CustomTagIconId; + onSelect: (icon: CustomTagIconId) => void; + disabled?: boolean; +}; + +export function CustomTagIconPicker({ selectedIcon, onSelect, disabled = false }: IconPickerProps) { + return ( +
+ {(Object.keys(ICON_MAP) as CustomTagIconId[]).map((iconId) => { + const IconComponent = ICON_MAP[iconId]; + const isSelected = iconId === selectedIcon; + + return ( + + ); + })} +
+ ); +} diff --git a/src/features/markets/components/market-indicators.tsx b/src/features/markets/components/market-indicators.tsx index 99ccdfad..68bf00a4 100644 --- a/src/features/markets/components/market-indicators.tsx +++ b/src/features/markets/components/market-indicators.tsx @@ -5,7 +5,8 @@ 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 { CustomTagIcon } from '@/components/shared/custom-tag-icons'; +import { useOfficialTrendingMarketKeys, useCustomTagMarketKeys, getMetricsKey, useEverLiquidated, useMarketMetricsMap } from '@/hooks/queries/useMarketMetricsQuery'; import { computeMarketWarnings } from '@/hooks/useMarketWarnings'; import { useMarketPreferences } from '@/stores/useMarketPreferences'; import type { Market } from '@/utils/types'; @@ -22,9 +23,20 @@ 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 trendingKeys = useTrendingMarketKeys(); - const isTrending = trendingConfig.enabled && trendingKeys.has(getMetricsKey(market.morphoBlue.chain.id, market.uniqueKey)); + const { showOfficialTrending, customTagConfig } = useMarketPreferences(); + const { metricsMap } = useMarketMetricsMap(); + + const marketKey = getMetricsKey(market.morphoBlue.chain.id, market.uniqueKey); + + // Official trending (backend-computed) + const officialTrendingKeys = useOfficialTrendingMarketKeys(); + const isOfficialTrending = showOfficialTrending && officialTrendingKeys.has(marketKey); + const trendingReason = metricsMap.get(marketKey)?.trendingReason; + + // User's custom tag + const customTagKeys = useCustomTagMarketKeys(); + const hasCustomTag = customTagConfig.enabled && customTagKeys.has(marketKey); + const warnings = showRisk ? computeMarketWarnings(market, true) : []; const hasWarnings = warnings.length > 0; const alertWarning = warnings.find((w) => w.level === 'alert'); @@ -101,7 +113,8 @@ export function MarketIndicators({ market, showRisk = false, isStared = false, h whitelisted={market.whitelisted} /> - {isTrending && ( + {/* Official Trending (backend-computed) */} + {isOfficialTrending && ( } - detail="This market is trending based on flow metrics" + detail={trendingReason ?? 'This market is trending'} /> } > @@ -124,6 +137,22 @@ export function MarketIndicators({ market, showRisk = false, isStared = false, h )} + {/* User's Custom Tag */} + {hasCustomTag && ( + } + detail="Matches your custom tag criteria" + /> + } + > +
+ +
+
+ )} + {showRisk && hasWarnings && ( - {trendingConfig.enabled && ( + {/* Official Trending Filter (backend-computed) */} + {showOfficialTrending && ( + + + )} + {/* Custom Tag Filter (user-defined) */} + {customTagConfig.enabled && ( +
diff --git a/src/hooks/queries/useMarketMetricsQuery.ts b/src/hooks/queries/useMarketMetricsQuery.ts index b49b9b9f..f46c6d65 100644 --- a/src/hooks/queries/useMarketMetricsQuery.ts +++ b/src/hooks/queries/useMarketMetricsQuery.ts @@ -1,10 +1,13 @@ import { useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; -import { useMarketPreferences, type TrendingConfig, type FlowTimeWindow } from '@/stores/useMarketPreferences'; +import { useMarketPreferences, type CustomTagConfig, type FlowTimeWindow } from '@/stores/useMarketPreferences'; // Re-export types for convenience export type { FlowTimeWindow } from '@/stores/useMarketPreferences'; +// Legacy alias +export type TrendingConfig = CustomTagConfig; + // Flow data for a specific time window export type MarketFlowData = { // Native token units (BigInt as string) - use loanAsset.decimals to convert @@ -40,6 +43,9 @@ export type MarketMetrics = { // Key flags everLiquidated: boolean; marketScore: number | null; + // Backend-computed trending (official) + isTrending: boolean; + trendingReason: string | null; // State and flows currentState: MarketCurrentState; flows: Record; @@ -127,18 +133,14 @@ const fetchAllMarketMetrics = async (params: MarketMetricsParams): Promise { const { chainId, sortBy, sortOrder, enabled = true } = params; @@ -156,13 +158,6 @@ export const useMarketMetricsQuery = (params: MarketMetricsParams = {}) => { /** * Returns a Map for O(1) lookup of market metrics by key. * Key format: `${chainId}-${uniqueKey.toLowerCase()}` - * - * @example - * ```tsx - * const { metricsMap, isLoading } = useMarketMetricsMap(); - * const metrics = metricsMap.get(getMetricsKey(chainId, uniqueKey)); - * if (metrics?.everLiquidated) { ... } - * ``` */ export const useMarketMetricsMap = (params: MarketMetricsParams = {}) => { const { data, isLoading, ...rest } = useMarketMetricsQuery(params); @@ -185,26 +180,24 @@ export const useMarketMetricsMap = (params: MarketMetricsParams = {}) => { /** * Convert flow assets (BigInt string) to human-readable number. - * @param flowAssets - The flow assets as BigInt string - * @param decimals - Token decimals */ export const parseFlowAssets = (flowAssets: string, decimals: number): number => { return Number(flowAssets) / 10 ** decimals; }; /** - * Determines if a market is trending based on flow thresholds. + * Check if a market matches a custom tag config. * All non-empty thresholds must be met (AND logic). * Only positive flows (inflows) are considered. */ -export const isMarketTrending = (metrics: MarketMetrics, trendingConfig: TrendingConfig): boolean => { - if (!trendingConfig.enabled) return false; +export const matchesCustomTag = (metrics: MarketMetrics, config: CustomTagConfig): boolean => { + if (!config.enabled) return false; - for (const [window, config] of Object.entries(trendingConfig.windows)) { - const supplyPct = config?.minSupplyFlowPct ?? ''; - const supplyUsd = config?.minSupplyFlowUsd ?? ''; - const borrowPct = config?.minBorrowFlowPct ?? ''; - const borrowUsd = config?.minBorrowFlowUsd ?? ''; + for (const [window, windowConfig] of Object.entries(config.windows)) { + const supplyPct = windowConfig?.minSupplyFlowPct ?? ''; + const supplyUsd = windowConfig?.minSupplyFlowUsd ?? ''; + const borrowPct = windowConfig?.minBorrowFlowPct ?? ''; + const borrowUsd = windowConfig?.minBorrowFlowUsd ?? ''; const hasSupplyThreshold = supplyPct || supplyUsd; const hasBorrowThreshold = borrowPct || borrowUsd; @@ -234,7 +227,7 @@ export const isMarketTrending = (metrics: MarketMetrics, trendingConfig: Trendin } } - const hasAnyThreshold = Object.values(trendingConfig.windows).some((c) => { + const hasAnyThreshold = Object.values(config.windows).some((c) => { const supplyPct = c?.minSupplyFlowPct ?? ''; const supplyUsd = c?.minSupplyFlowUsd ?? ''; const borrowPct = c?.minBorrowFlowPct ?? ''; @@ -245,30 +238,52 @@ export const isMarketTrending = (metrics: MarketMetrics, trendingConfig: Trendin return hasAnyThreshold; }; +// Legacy alias +export const isMarketTrending = matchesCustomTag; + +/** + * Returns a Set of market keys that are officially trending (backend-computed). + * Uses isTrending field from Monarch API. + */ +export const useOfficialTrendingMarketKeys = () => { + 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 that are currently trending. - * Uses metricsMap for O(1) lookup and filters based on trending config from preferences. + * Returns a Set of market keys matching user's custom tag config. */ -export const useTrendingMarketKeys = () => { +export const useCustomTagMarketKeys = () => { const { metricsMap } = useMarketMetricsMap(); - const { trendingConfig } = useMarketPreferences(); + const { customTagConfig } = useMarketPreferences(); return useMemo(() => { const keys = new Set(); - if (!trendingConfig.enabled) return keys; + if (!customTagConfig.enabled) return keys; for (const [key, metrics] of metricsMap) { - if (isMarketTrending(metrics, trendingConfig)) { + if (matchesCustomTag(metrics, customTagConfig)) { keys.add(key); } } return keys; - }, [metricsMap, trendingConfig]); + }, [metricsMap, customTagConfig]); }; +// Legacy alias - now returns official trending (breaking change, but intended) +export const useTrendingMarketKeys = useOfficialTrendingMarketKeys; + /** * Returns whether a market has ever been liquidated. - * Uses everLiquidated field from Monarch API market metrics. */ export const useEverLiquidated = (chainId: number, uniqueKey: string): boolean => { const { metricsMap } = useMarketMetricsMap(); diff --git a/src/hooks/useFilteredMarkets.ts b/src/hooks/useFilteredMarkets.ts index 53ce30be..8658533c 100644 --- a/src/hooks/useFilteredMarkets.ts +++ b/src/hooks/useFilteredMarkets.ts @@ -5,7 +5,7 @@ import { useMarketPreferences } from '@/stores/useMarketPreferences'; import { useAppSettings } from '@/stores/useAppSettings'; import { useTrustedVaults } from '@/stores/useTrustedVaults'; import { useTokensQuery } from '@/hooks/queries/useTokensQuery'; -import { useTrendingMarketKeys, getMetricsKey } from '@/hooks/queries/useMarketMetricsQuery'; +import { useOfficialTrendingMarketKeys, useCustomTagMarketKeys, getMetricsKey } from '@/hooks/queries/useMarketMetricsQuery'; import { filterMarkets, sortMarkets, createPropertySort, createStarredSort } from '@/utils/marketFilters'; import { SortColumn } from '@/features/markets/components/constants'; import { getVaultKey } from '@/constants/vaults/known_vaults'; @@ -18,7 +18,8 @@ export const useFilteredMarkets = (): Market[] => { const { showUnwhitelistedMarkets } = useAppSettings(); const { vaults: trustedVaults } = useTrustedVaults(); const { findToken } = useTokensQuery(); - const trendingKeys = useTrendingMarketKeys(); + const officialTrendingKeys = useOfficialTrendingMarketKeys(); + const customTagKeys = useCustomTagMarketKeys(); return useMemo(() => { let markets = showUnwhitelistedMarkets ? allMarkets : whitelistedMarkets; @@ -62,10 +63,19 @@ export const useFilteredMarkets = (): Market[] => { }); } - if (filters.trendingMode && trendingKeys.size > 0) { + // Official trending filter (backend-computed) + if (filters.trendingMode && officialTrendingKeys.size > 0) { markets = markets.filter((market) => { const key = getMetricsKey(market.morphoBlue.chain.id, market.uniqueKey); - return trendingKeys.has(key); + return officialTrendingKeys.has(key); + }); + } + + // Custom tag filter (user-defined) + if (filters.customTagMode && customTagKeys.size > 0) { + markets = markets.filter((market) => { + const key = getMetricsKey(market.morphoBlue.chain.id, market.uniqueKey); + return customTagKeys.has(key); }); } @@ -110,5 +120,5 @@ export const useFilteredMarkets = (): Market[] => { } return markets; - }, [allMarkets, whitelistedMarkets, showUnwhitelistedMarkets, filters, preferences, trustedVaults, findToken, trendingKeys]); + }, [allMarkets, whitelistedMarkets, showUnwhitelistedMarkets, filters, preferences, trustedVaults, findToken, officialTrendingKeys, customTagKeys]); }; diff --git a/src/modals/settings/monarch-settings/constants.ts b/src/modals/settings/monarch-settings/constants.ts index 8645cb36..d5ea3735 100644 --- a/src/modals/settings/monarch-settings/constants.ts +++ b/src/modals/settings/monarch-settings/constants.ts @@ -23,7 +23,7 @@ export const SETTINGS_CATEGORIES: CategoryConfig[] = [ ]; export const DETAIL_TITLES: Record, string> = { - 'trending-config': 'Configure Trending', + 'trending-config': 'Configure Custom Tag', 'trusted-vaults': 'Trusted Vaults', 'blacklisted-markets': 'Blacklisted Markets', 'rpc-config': 'Custom RPC', diff --git a/src/modals/settings/monarch-settings/details/TrendingDetail.tsx b/src/modals/settings/monarch-settings/details/TrendingDetail.tsx index ce8b1019..25b3cce9 100644 --- a/src/modals/settings/monarch-settings/details/TrendingDetail.tsx +++ b/src/modals/settings/monarch-settings/details/TrendingDetail.tsx @@ -4,10 +4,11 @@ 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 CustomTagWindowConfig } from '@/stores/useMarketPreferences'; +import { useMarketMetricsMap, matchesCustomTag, getMetricsKey } from '@/hooks/queries/useMarketMetricsQuery'; import { useProcessedMarkets } from '@/hooks/useProcessedMarkets'; import { formatReadable } from '@/utils/balance'; +import { CustomTagIconPicker } from '@/components/shared/custom-tag-icons'; import type { Market } from '@/utils/types'; const TIME_WINDOWS: { value: FlowTimeWindow; label: string }[] = [ @@ -17,8 +18,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; windows: Record }): string { + if (!config.enabled) return 'Custom tag is disabled'; const parts: string[] = []; @@ -91,10 +92,10 @@ function CompactInput({ } export function TrendingDetail() { - const { trendingConfig, setTrendingEnabled, setTrendingWindowConfig } = useMarketPreferences(); + const { customTagConfig, setCustomTagEnabled, setCustomTagIcon, setCustomTagWindowConfig } = useMarketPreferences(); const { metricsMap } = useMarketMetricsMap(); const { allMarkets } = useProcessedMarkets(); - const isEnabled = trendingConfig.enabled; + const isEnabled = customTagConfig.enabled; const matchingMarkets = useMemo(() => { if (!isEnabled || metricsMap.size === 0) return []; @@ -102,7 +103,7 @@ export function TrendingDetail() { const matches: Array<{ market: Market; supplyFlowPct1h: number }> = []; for (const [key, metrics] of metricsMap) { - if (isMarketTrending(metrics, trendingConfig)) { + if (matchesCustomTag(metrics, customTagConfig)) { const market = allMarkets.find((m) => getMetricsKey(m.morphoBlue.chain.id, m.uniqueKey) === key); if (market) { matches.push({ @@ -114,18 +115,28 @@ export function TrendingDetail() { } return matches.sort((a, b) => (b.market.state?.supplyAssetsUsd ?? 0) - (a.market.state?.supplyAssetsUsd ?? 0)); - }, [isEnabled, metricsMap, trendingConfig, allMarkets]); + }, [isEnabled, metricsMap, customTagConfig, allMarkets]); const totalMatches = matchingMarkets.length; - const handleChange = (window: FlowTimeWindow, field: keyof TrendingWindowConfig, value: string) => { - setTrendingWindowConfig(window, { [field]: value }); + const handleChange = (window: FlowTimeWindow, field: keyof CustomTagWindowConfig, value: string) => { + setCustomTagWindowConfig(window, { [field]: value }); }; - const filterSummary = generateFilterSummary(trendingConfig); + const filterSummary = generateFilterSummary(customTagConfig); return (
+ {/* Icon Picker */} +
+ Choose an icon for your custom tag: + +
+ {/* Toggle + Summary */}
@@ -133,7 +144,7 @@ export function TrendingDetail() {
@@ -150,7 +161,7 @@ export function TrendingDetail() { {/* Rows */} {TIME_WINDOWS.map(({ value: window, label }) => { - const config = trendingConfig.windows[window]; + const config = customTagConfig.windows[window]; return (
+ {/* Official Trending */}
-

Trending

+

🔥 Official Trending

+
+ + {/* Custom Tags */} +
+

🏷️ Custom Tags

+ onNavigateToDetail?.('trending-config')} />
+ {/* Liquidity Sourcing */}

Liquidity Sourcing

+ {/* Developer */}

Developer

; + icon: CustomTagIconId; // User-selected icon for their custom tag + windows: Record; }; -const DEFAULT_TRENDING_CONFIG: TrendingConfig = { +const DEFAULT_CUSTOM_TAG_CONFIG: CustomTagConfig = { enabled: false, + icon: 'bookmark', windows: { '1h': { minSupplyFlowPct: '6', minSupplyFlowUsd: '', minBorrowFlowPct: '', minBorrowFlowUsd: '' }, - '24h': { minSupplyFlowPct: '', minSupplyFlowUsd: '', minBorrowFlowPct: '', minBorrowFlowUsd: '' }, - '7d': { minSupplyFlowPct: '', minSupplyFlowUsd: '', minBorrowFlowPct: '', minBorrowFlowUsd: '' }, + '24h': { minSupplyFlowPct: '3', minSupplyFlowUsd: '', minBorrowFlowPct: '2', minBorrowFlowUsd: '' }, + '7d': { minSupplyFlowPct: '5', minSupplyFlowUsd: '', minBorrowFlowPct: '1', minBorrowFlowUsd: '' }, '30d': { minSupplyFlowPct: '', minSupplyFlowUsd: '', minBorrowFlowPct: '', minBorrowFlowUsd: '' }, }, }; +// Legacy alias for backwards compatibility +export type TrendingWindowConfig = CustomTagWindowConfig; +export type TrendingConfig = CustomTagConfig; + type MarketPreferencesState = { // Sorting sortColumn: SortColumn; @@ -60,8 +75,14 @@ type MarketPreferencesState = { minBorrowEnabled: boolean; minLiquidityEnabled: boolean; - // Trending Config (Beta) - trendingConfig: TrendingConfig; + // Official Trending (backend-computed) + showOfficialTrending: boolean; // Default ON - show 🔥 for API's isTrending + + // Custom Tags (user-defined) + customTagConfig: CustomTagConfig; + + // Legacy alias (points to customTagConfig) + trendingConfig: CustomTagConfig; }; type MarketPreferencesActions = { @@ -95,9 +116,17 @@ type MarketPreferencesActions = { setMinBorrowEnabled: (enabled: boolean) => void; setMinLiquidityEnabled: (enabled: boolean) => void; - // Trending Config (Beta) + // Official Trending + setShowOfficialTrending: (show: boolean) => void; + + // Custom Tags + setCustomTagEnabled: (enabled: boolean) => void; + setCustomTagIcon: (icon: CustomTagIconId) => void; + setCustomTagWindowConfig: (window: FlowTimeWindow, config: Partial) => void; + + // Legacy aliases setTrendingEnabled: (enabled: boolean) => void; - setTrendingWindowConfig: (window: FlowTimeWindow, config: Partial) => void; + setTrendingWindowConfig: (window: FlowTimeWindow, config: Partial) => void; // Bulk update for migration setAll: (state: Partial) => void; @@ -134,7 +163,12 @@ export const useMarketPreferences = create()( minSupplyEnabled: false, minBorrowEnabled: false, minLiquidityEnabled: false, - trendingConfig: DEFAULT_TRENDING_CONFIG, + showOfficialTrending: true, // Default ON + customTagConfig: DEFAULT_CUSTOM_TAG_CONFIG, + // Legacy alias getter + get trendingConfig() { + return this.customTagConfig; + }, setSortColumn: (column) => set({ sortColumn: column }), setSortDirection: (direction) => set({ sortDirection: direction }), @@ -166,20 +200,46 @@ export const useMarketPreferences = create()( setMinSupplyEnabled: (enabled) => set({ minSupplyEnabled: enabled }), setMinBorrowEnabled: (enabled) => set({ minBorrowEnabled: enabled }), setMinLiquidityEnabled: (enabled) => set({ minLiquidityEnabled: enabled }), + + // Official Trending + setShowOfficialTrending: (show) => set({ showOfficialTrending: show }), + + // Custom Tags + setCustomTagEnabled: (enabled) => + set((state) => ({ + customTagConfig: { ...state.customTagConfig, enabled }, + })), + setCustomTagIcon: (icon) => + set((state) => ({ + customTagConfig: { ...state.customTagConfig, icon }, + })), + setCustomTagWindowConfig: (window, config) => + set((state) => ({ + customTagConfig: { + ...state.customTagConfig, + windows: { + ...state.customTagConfig.windows, + [window]: { ...state.customTagConfig.windows[window], ...config }, + }, + }, + })), + + // Legacy aliases (point to custom tag methods) setTrendingEnabled: (enabled) => set((state) => ({ - trendingConfig: { ...state.trendingConfig, enabled }, + customTagConfig: { ...state.customTagConfig, enabled }, })), setTrendingWindowConfig: (window, config) => set((state) => ({ - trendingConfig: { - ...state.trendingConfig, + customTagConfig: { + ...state.customTagConfig, windows: { - ...state.trendingConfig.windows, - [window]: { ...state.trendingConfig.windows[window], ...config }, + ...state.customTagConfig.windows, + [window]: { ...state.customTagConfig.windows[window], ...config }, }, }, })), + setAll: (state) => set(state), }), { diff --git a/src/stores/useMarketsFilters.ts b/src/stores/useMarketsFilters.ts index e0797469..44520c5a 100644 --- a/src/stores/useMarketsFilters.ts +++ b/src/stores/useMarketsFilters.ts @@ -15,7 +15,8 @@ type MarketsFiltersState = { selectedNetwork: SupportedNetworks | null; selectedOracles: PriceFeedVendors[]; searchQuery: string; - trendingMode: boolean; // Filter toggle - thresholds are in useMarketPreferences + trendingMode: boolean; // Official trending filter (backend-computed) + customTagMode: boolean; // User's custom tag filter }; type MarketsFiltersActions = { @@ -25,6 +26,7 @@ type MarketsFiltersActions = { setSelectedOracles: (oracles: PriceFeedVendors[]) => void; setSearchQuery: (query: string) => void; toggleTrendingMode: () => void; + toggleCustomTagMode: () => void; resetFilters: () => void; }; @@ -37,6 +39,7 @@ const DEFAULT_STATE: MarketsFiltersState = { selectedOracles: [], searchQuery: '', trendingMode: false, + customTagMode: false, }; /** @@ -57,6 +60,7 @@ export const useMarketsFilters = create()((set) => ({ setSelectedOracles: (oracles) => set({ selectedOracles: oracles }), setSearchQuery: (query) => set({ searchQuery: query }), toggleTrendingMode: () => set((state) => ({ trendingMode: !state.trendingMode })), + toggleCustomTagMode: () => set((state) => ({ customTagMode: !state.customTagMode })), resetFilters: () => set(DEFAULT_STATE), })); From 0b6b129dd78c5eb3e80b0241f7285345a7bb7a5f Mon Sep 17 00:00:00 2001 From: anton Date: Thu, 29 Jan 2026 16:41:56 +0800 Subject: [PATCH 2/9] fix: clean up styling - remove emojis, improve UX - Remove emojis from settings panel titles/descriptions - Remove emoji from filter label - Reduce custom tag icons from 20 to 10 (most useful ones) - Improve custom tag config UX: toggle first, config below when enabled - Use standard tooltip format with title + detail - Consistent icon sizing --- src/components/shared/custom-tag-icons.tsx | 40 ++-- .../markets/components/market-indicators.tsx | 8 +- .../components/markets-filter-compact.tsx | 2 +- .../details/TrendingDetail.tsx | 225 +++++++++--------- .../panels/ExperimentalPanel.tsx | 26 +- src/stores/useMarketPreferences.ts | 6 +- 6 files changed, 155 insertions(+), 152 deletions(-) diff --git a/src/components/shared/custom-tag-icons.tsx b/src/components/shared/custom-tag-icons.tsx index b90d3dac..0f9f6a24 100644 --- a/src/components/shared/custom-tag-icons.tsx +++ b/src/components/shared/custom-tag-icons.tsx @@ -1,11 +1,9 @@ 'use client'; import type { IconType } from 'react-icons'; -import { AiOutlineFire, AiOutlineRocket, AiOutlineStar, AiOutlineThunderbolt, AiOutlineEye, AiOutlineHeart, AiOutlineTrophy, AiOutlineCrown } from 'react-icons/ai'; -import { BiTrendingUp, BiTargetLock, BiBookmark, BiFlag } from 'react-icons/bi'; -import { FaGem, FaCoins, FaBolt, FaChartLine } from 'react-icons/fa'; -import { HiOutlineSparkles, HiOutlineLightningBolt } from 'react-icons/hi'; -import { IoFlameOutline, IoDiamondOutline } from 'react-icons/io5'; +import { AiOutlineStar, AiOutlineThunderbolt, AiOutlineEye, AiOutlineHeart, AiOutlineRocket } from 'react-icons/ai'; +import { BiTargetLock, BiBookmark, BiFlag } from 'react-icons/bi'; +import { FaGem, FaChartLine } from 'react-icons/fa'; import type { CustomTagIconId } from '@/stores/useMarketPreferences'; /** @@ -13,26 +11,16 @@ import type { CustomTagIconId } from '@/stores/useMarketPreferences'; * Keep this in sync with CUSTOM_TAG_ICONS in useMarketPreferences. */ export const ICON_MAP: Record = { - fire: AiOutlineFire, - rocket: AiOutlineRocket, star: AiOutlineStar, - bolt: AiOutlineThunderbolt, - gem: FaGem, - chart: FaChartLine, - target: BiTargetLock, - eye: AiOutlineEye, bookmark: BiBookmark, flag: BiFlag, + target: BiTargetLock, + eye: AiOutlineEye, + gem: FaGem, + bolt: AiOutlineThunderbolt, + chart: FaChartLine, + rocket: AiOutlineRocket, heart: AiOutlineHeart, - coins: FaCoins, - trophy: AiOutlineTrophy, - zap: FaBolt, - trending: BiTrendingUp, - sparkles: HiOutlineSparkles, - flame: IoFlameOutline, - diamond: IoDiamondOutline, - crown: AiOutlineCrown, - lightning: HiOutlineLightningBolt, }; type CustomTagIconProps = { @@ -61,7 +49,7 @@ type IconPickerProps = { export function CustomTagIconPicker({ selectedIcon, onSelect, disabled = false }: IconPickerProps) { return ( -
+
{(Object.keys(ICON_MAP) as CustomTagIconId[]).map((iconId) => { const IconComponent = ICON_MAP[iconId]; const isSelected = iconId === selectedIcon; @@ -72,11 +60,11 @@ export function CustomTagIconPicker({ selectedIcon, onSelect, disabled = false } type="button" onClick={() => onSelect(iconId)} disabled={disabled} - className={`flex h-8 w-8 items-center justify-center rounded-md transition-all ${ + className={`flex h-8 w-8 items-center justify-center rounded-md border transition-all ${ isSelected - ? 'bg-primary/20 ring-2 ring-primary text-primary' - : 'bg-surface hover:bg-default-100 text-secondary' - } ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`} + ? 'border-primary bg-primary/10 text-primary' + : 'border-border bg-surface text-secondary hover:border-primary/50 hover:text-primary' + } ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`} title={iconId} > diff --git a/src/features/markets/components/market-indicators.tsx b/src/features/markets/components/market-indicators.tsx index 68bf00a4..68b6d921 100644 --- a/src/features/markets/components/market-indicators.tsx +++ b/src/features/markets/components/market-indicators.tsx @@ -120,17 +120,18 @@ export function MarketIndicators({ market, showRisk = false, isStared = false, h } - detail={trendingReason ?? 'This market is trending'} + title="Trending" + detail={trendingReason ?? 'This market is trending based on flow activity'} /> } >
@@ -143,6 +144,7 @@ export function MarketIndicators({ market, showRisk = false, isStared = false, h content={ } + title="Custom Tag" detail="Matches your custom tag criteria" /> } diff --git a/src/features/positions/components/markets-filter-compact.tsx b/src/features/positions/components/markets-filter-compact.tsx index 0900cbd2..23f4e330 100644 --- a/src/features/positions/components/markets-filter-compact.tsx +++ b/src/features/positions/components/markets-filter-compact.tsx @@ -248,7 +248,7 @@ export function MarketFilter({ className, variant = 'ghost' }: MarketFilterProps {/* Official Trending Filter (backend-computed) */} {showOfficialTrending && ( }): string { - if (!config.enabled) return 'Custom tag is disabled'; + if (!config.enabled) return 'Disabled'; const parts: string[] = []; @@ -35,7 +35,7 @@ function generateFilterSummary(config: { enabled: boolean; windows: Record 0) { - windowParts.push(`supply grew ${supplyParts.join(' and ')}`); + windowParts.push(`supply ${supplyParts.join(' & ')}`); } const borrowPct = windowConfig.minBorrowFlowPct ?? ''; @@ -44,16 +44,16 @@ function generateFilterSummary(config: { enabled: boolean; windows: Record 0) { - windowParts.push(`borrow grew ${borrowParts.join(' and ')}`); + windowParts.push(`borrow ${borrowParts.join(' & ')}`); } if (windowParts.length > 0) { - parts.push(`${windowParts.join(', ')} in ${label}`); + parts.push(`${label}: ${windowParts.join(', ')}`); } } - if (parts.length === 0) return 'No thresholds configured'; - return `Markets where ${parts.join('; ')}`; + if (parts.length === 0) return 'No thresholds set'; + return parts.join(' | '); } function CompactInput({ @@ -127,120 +127,131 @@ export function TrendingDetail() { return (
- {/* Icon Picker */} -
- Choose an icon for your custom tag: - -
- - {/* Toggle + Summary */} -
-
-

{filterSummary}

+ {/* Enable Toggle - Primary action at top */} +
+
+
+ +
+
+

Custom Tag

+

{filterSummary}

+
- {/* Compact threshold table */} -
- {/* Header */} -
-
-
Supply Flow
-
Borrow Flow
-
+ {/* Configuration - Only show when enabled */} + {isEnabled && ( + <> + {/* Icon Picker */} +
+ Choose Icon + +
- {/* Rows */} - {TIME_WINDOWS.map(({ value: window, label }) => { - const config = customTagConfig.windows[window]; - - return ( -
-
{label}
- - {/* Supply inputs */} -
- handleChange(window, 'minSupplyFlowPct', v)} - disabled={!isEnabled} - suffix="%" - /> - handleChange(window, 'minSupplyFlowUsd', v.replace(/[^0-9]/g, ''))} - disabled={!isEnabled} - prefix="$" - /> -
- - {/* Borrow inputs */} -
- handleChange(window, 'minBorrowFlowPct', v)} - disabled={!isEnabled} - suffix="%" - /> - handleChange(window, 'minBorrowFlowUsd', v.replace(/[^0-9]/g, ''))} - disabled={!isEnabled} - prefix="$" - /> -
+ {/* Compact threshold table */} +
+
+ Flow Thresholds +
+ {/* Header */} +
+
+
Supply
+
Borrow
- ); - })} -
- {/* Preview */} - {isEnabled && ( -
-
- Preview - - {totalMatches > 0 ? `${totalMatches} market${totalMatches !== 1 ? 's' : ''} match` : 'No matches'} - -
-
- {matchingMarkets.length > 0 ? ( -
- {matchingMarkets.slice(0, 2).map((m) => ( -
- { + const config = customTagConfig.windows[window]; + + return ( +
+
{label}
+ + {/* Supply inputs */} +
+ handleChange(window, 'minSupplyFlowPct', v)} + disabled={false} + suffix="%" + /> + handleChange(window, 'minSupplyFlowUsd', v.replace(/[^0-9]/g, ''))} + disabled={false} + prefix="$" + /> +
+ + {/* Borrow inputs */} +
+ handleChange(window, 'minBorrowFlowPct', v)} + disabled={false} + suffix="%" + /> + handleChange(window, 'minBorrowFlowUsd', v.replace(/[^0-9]/g, ''))} + disabled={false} + prefix="$" /> - +{m.supplyFlowPct1h.toFixed(1)}%
- ))} - {totalMatches > 2 && +{totalMatches - 2} more} -
- ) : ( - No markets match current criteria - )} +
+ ); + })}
-
+ + {/* Preview */} +
+
+ Preview + + {totalMatches > 0 ? `${totalMatches} match${totalMatches !== 1 ? 'es' : ''}` : 'No matches'} + +
+
+ {matchingMarkets.length > 0 ? ( +
+ {matchingMarkets.slice(0, 3).map((m) => ( +
+ + +{m.supplyFlowPct1h.toFixed(1)}% +
+ ))} + {totalMatches > 3 && +{totalMatches - 3} more} +
+ ) : ( + No markets match current criteria + )} +
+
+ )}
); diff --git a/src/modals/settings/monarch-settings/panels/ExperimentalPanel.tsx b/src/modals/settings/monarch-settings/panels/ExperimentalPanel.tsx index 983d9026..a59c04de 100644 --- a/src/modals/settings/monarch-settings/panels/ExperimentalPanel.tsx +++ b/src/modals/settings/monarch-settings/panels/ExperimentalPanel.tsx @@ -18,10 +18,10 @@ export function ExperimentalPanel({ onNavigateToDetail }: ExperimentalPanelProps
{/* Official Trending */}
-

🔥 Official Trending

+

Official Trending

-

🏷️ Custom Tags

+

Custom Tags

- - onNavigateToDetail?.('trending-config')} - /> + {customTagConfig.enabled && ( + <> + + onNavigateToDetail?.('trending-config')} + /> + + )}
{/* Liquidity Sourcing */} diff --git a/src/stores/useMarketPreferences.ts b/src/stores/useMarketPreferences.ts index 5791d321..43c22cec 100644 --- a/src/stores/useMarketPreferences.ts +++ b/src/stores/useMarketPreferences.ts @@ -18,10 +18,8 @@ export type CustomTagWindowConfig = { // Available icons for custom tags (react-icons identifiers) export const CUSTOM_TAG_ICONS = [ - 'fire', 'rocket', 'star', 'bolt', 'gem', - 'chart', 'target', 'eye', 'bookmark', 'flag', - 'heart', 'coins', 'trophy', 'zap', 'trending', - 'sparkles', 'flame', 'diamond', 'crown', 'lightning', + 'star', 'bookmark', 'flag', 'target', 'eye', + 'gem', 'bolt', 'chart', 'rocket', 'heart', ] as const; export type CustomTagIconId = (typeof CUSTOM_TAG_ICONS)[number]; From 0532812360ed54bb77a1bb0f6d8b775c986d2069 Mon Sep 17 00:00:00 2001 From: anton Date: Thu, 29 Jan 2026 17:02:13 +0800 Subject: [PATCH 3/9] feat: simplify custom tag criteria - Remove USD fields, percentage-only thresholds - Support negative percentages for detecting outflows - Positive (5): actual >= 5% (growth) - Negative (-3): actual <= -3% (decline) - Cleaner UI: 3-column table (Period, Supply %, Borrow %) - Better input handling for negative decimals - Show red/green colors in preview based on direction --- src/hooks/queries/useMarketMetricsQuery.ts | 51 +++--- .../details/TrendingDetail.tsx | 159 ++++++++---------- src/stores/useMarketPreferences.ts | 18 +- 3 files changed, 103 insertions(+), 125 deletions(-) diff --git a/src/hooks/queries/useMarketMetricsQuery.ts b/src/hooks/queries/useMarketMetricsQuery.ts index f46c6d65..9a9db80d 100644 --- a/src/hooks/queries/useMarketMetricsQuery.ts +++ b/src/hooks/queries/useMarketMetricsQuery.ts @@ -188,51 +188,50 @@ export const parseFlowAssets = (flowAssets: string, decimals: number): number => /** * Check if a market matches a custom tag config. * All non-empty thresholds must be met (AND logic). - * Only positive flows (inflows) are considered. + * Supports both positive (inflows) and negative (outflows) thresholds. + * + * Logic: + * - Positive threshold (e.g., "5"): actual >= threshold (growth of 5% or more) + * - Negative threshold (e.g., "-3"): actual <= threshold (decline of 3% or more) */ export const matchesCustomTag = (metrics: MarketMetrics, config: CustomTagConfig): boolean => { if (!config.enabled) return false; for (const [window, windowConfig] of Object.entries(config.windows)) { - const supplyPct = windowConfig?.minSupplyFlowPct ?? ''; - const supplyUsd = windowConfig?.minSupplyFlowUsd ?? ''; - const borrowPct = windowConfig?.minBorrowFlowPct ?? ''; - const borrowUsd = windowConfig?.minBorrowFlowUsd ?? ''; + const supplyPct = windowConfig?.supplyFlowPct ?? ''; + const borrowPct = windowConfig?.borrowFlowPct ?? ''; - const hasSupplyThreshold = supplyPct || supplyUsd; - const hasBorrowThreshold = borrowPct || borrowUsd; + const hasSupplyThreshold = supplyPct !== ''; + const hasBorrowThreshold = borrowPct !== ''; if (!hasSupplyThreshold && !hasBorrowThreshold) continue; const flow = metrics.flows[window as FlowTimeWindow]; if (!flow) return false; - if (supplyPct) { - const actualPct = flow.supplyFlowPct ?? 0; - if (actualPct < Number(supplyPct)) return false; - } - if (supplyUsd) { - const actualUsd = flow.supplyFlowUsd ?? 0; - if (actualUsd < Number(supplyUsd)) return false; + // Check supply threshold + if (hasSupplyThreshold) { + const threshold = Number(supplyPct); + const actual = flow.supplyFlowPct ?? 0; + // Positive threshold: actual must be >= threshold + // Negative threshold: actual must be <= threshold (more negative) + if (threshold >= 0 && actual < threshold) return false; + if (threshold < 0 && actual > threshold) return false; } - if (borrowPct) { + // Check borrow threshold + if (hasBorrowThreshold) { + const threshold = Number(borrowPct); const borrowBase = metrics.currentState.borrowUsd; - const actualPct = borrowBase > 0 ? ((flow.borrowFlowUsd ?? 0) / borrowBase) * 100 : 0; - if (actualPct < Number(borrowPct)) return false; - } - if (borrowUsd) { - const actualUsd = flow.borrowFlowUsd ?? 0; - if (actualUsd < Number(borrowUsd)) return false; + const actual = borrowBase > 0 ? ((flow.borrowFlowUsd ?? 0) / borrowBase) * 100 : 0; + if (threshold >= 0 && actual < threshold) return false; + if (threshold < 0 && actual > threshold) return false; } } + // Must have at least one threshold set const hasAnyThreshold = Object.values(config.windows).some((c) => { - const supplyPct = c?.minSupplyFlowPct ?? ''; - const supplyUsd = c?.minSupplyFlowUsd ?? ''; - const borrowPct = c?.minBorrowFlowPct ?? ''; - const borrowUsd = c?.minBorrowFlowUsd ?? ''; - return supplyPct || supplyUsd || borrowPct || borrowUsd; + return (c?.supplyFlowPct ?? '') !== '' || (c?.borrowFlowPct ?? '') !== ''; }); return hasAnyThreshold; diff --git a/src/modals/settings/monarch-settings/details/TrendingDetail.tsx b/src/modals/settings/monarch-settings/details/TrendingDetail.tsx index a4085cf9..ea437738 100644 --- a/src/modals/settings/monarch-settings/details/TrendingDetail.tsx +++ b/src/modals/settings/monarch-settings/details/TrendingDetail.tsx @@ -7,17 +7,22 @@ import { MarketIdentity, MarketIdentityMode } from '@/features/markets/component import { useMarketPreferences, type FlowTimeWindow, type CustomTagWindowConfig } from '@/stores/useMarketPreferences'; import { useMarketMetricsMap, matchesCustomTag, getMetricsKey } from '@/hooks/queries/useMarketMetricsQuery'; import { useProcessedMarkets } from '@/hooks/useProcessedMarkets'; -import { formatReadable } from '@/utils/balance'; import { CustomTagIconPicker, CustomTagIcon } from '@/components/shared/custom-tag-icons'; import type { Market } from '@/utils/types'; const TIME_WINDOWS: { value: FlowTimeWindow; label: string }[] = [ - { value: '1h', label: '1h' }, - { value: '24h', label: '24h' }, - { value: '7d', label: '7d' }, - { value: '30d', label: '30d' }, + { value: '1h', label: '1 Hour' }, + { value: '24h', label: '24 Hours' }, + { value: '7d', label: '7 Days' }, + { value: '30d', label: '30 Days' }, ]; +function formatThreshold(value: string): string { + if (!value || value === '') return '-'; + const num = Number(value); + return num >= 0 ? `≥${value}%` : `≤${value}%`; +} + function generateFilterSummary(config: { enabled: boolean; windows: Record }): string { if (!config.enabled) return 'Disabled'; @@ -27,66 +32,48 @@ function generateFilterSummary(config: { enabled: boolean; windows: Record 0) { - windowParts.push(`supply ${supplyParts.join(' & ')}`); - } + if (!supply && !borrow) continue; - const borrowPct = windowConfig.minBorrowFlowPct ?? ''; - const borrowUsd = windowConfig.minBorrowFlowUsd ?? ''; - const borrowParts: string[] = []; - if (borrowPct) borrowParts.push(`+${borrowPct}%`); - if (borrowUsd) borrowParts.push(`+$${formatReadable(Number(borrowUsd))}`); - if (borrowParts.length > 0) { - windowParts.push(`borrow ${borrowParts.join(' & ')}`); - } + const conditions: string[] = []; + if (supply) conditions.push(`Supply ${formatThreshold(supply)}`); + if (borrow) conditions.push(`Borrow ${formatThreshold(borrow)}`); - if (windowParts.length > 0) { - parts.push(`${label}: ${windowParts.join(', ')}`); - } + parts.push(`${label}: ${conditions.join(', ')}`); } if (parts.length === 0) return 'No thresholds set'; return parts.join(' | '); } -function CompactInput({ +function PercentInput({ value, onChange, - disabled, - prefix, - suffix, + placeholder = '0', }: { value: string; onChange: (v: string) => void; - disabled: boolean; - prefix?: string; - suffix?: string; + placeholder?: string; }) { return ( -
- {prefix && {prefix}} +
{ - const stripped = e.target.value.replace(/[^0-9.]/g, ''); - const parts = stripped.split('.'); - const result = parts.length <= 1 ? stripped : `${parts[0]}.${parts.slice(1).join('')}`; - onChange(result); + // Allow negative numbers and decimals + const v = e.target.value; + if (v === '' || v === '-' || /^-?\d*\.?\d*$/.test(v)) { + onChange(v); + } }} - placeholder="-" - disabled={disabled} - className="font-inter h-6 w-12 px-1 text-center text-xs" + placeholder={placeholder} + className="font-inter h-8 w-16 px-2 text-center text-sm" /> - {suffix && {suffix}} + %
); } @@ -100,15 +87,21 @@ export function TrendingDetail() { const matchingMarkets = useMemo(() => { if (!isEnabled || metricsMap.size === 0) return []; - const matches: Array<{ market: Market; supplyFlowPct1h: number }> = []; + const matches: Array<{ market: Market; supplyFlowPct: number; window: string }> = []; for (const [key, metrics] of metricsMap) { if (matchesCustomTag(metrics, customTagConfig)) { const market = allMarkets.find((m) => getMetricsKey(m.morphoBlue.chain.id, m.uniqueKey) === key); if (market) { + // Find which window matched for display + const matchedWindow = TIME_WINDOWS.find(({ value }) => { + const cfg = customTagConfig.windows[value]; + return cfg.supplyFlowPct || cfg.borrowFlowPct; + }); matches.push({ market, - supplyFlowPct1h: metrics.flows['1h']?.supplyFlowPct ?? 0, + supplyFlowPct: metrics.flows['24h']?.supplyFlowPct ?? 0, + window: matchedWindow?.label ?? '24h', }); } } @@ -135,7 +128,7 @@ export function TrendingDetail() {

Custom Tag

-

{filterSummary}

+

{filterSummary}

{/* Icon Picker */}
- Choose Icon + Icon
- {/* Compact threshold table */} + {/* Threshold table - simplified */}
-
+
Flow Thresholds +

+ Use positive values for growth (≥5%), negative for decline (≤-3%) +

+ {/* Header */} -
-
-
Supply
-
Borrow
+
+
Period
+
Supply
+
Borrow
{/* Rows */} @@ -177,39 +174,19 @@ export function TrendingDetail() { return (
-
{label}
- - {/* Supply inputs */} -
- handleChange(window, 'minSupplyFlowPct', v)} - disabled={false} - suffix="%" - /> - handleChange(window, 'minSupplyFlowUsd', v.replace(/[^0-9]/g, ''))} - disabled={false} - prefix="$" +
{label}
+
+ handleChange(window, 'supplyFlowPct', v)} />
- - {/* Borrow inputs */} -
- handleChange(window, 'minBorrowFlowPct', v)} - disabled={false} - suffix="%" - /> - handleChange(window, 'minBorrowFlowUsd', v.replace(/[^0-9]/g, ''))} - disabled={false} - prefix="$" +
+ handleChange(window, 'borrowFlowPct', v)} />
@@ -219,15 +196,15 @@ export function TrendingDetail() { {/* Preview */}
-
- Preview +
+ Preview {totalMatches > 0 ? `${totalMatches} match${totalMatches !== 1 ? 'es' : ''}` : 'No matches'}
-
+
{matchingMarkets.length > 0 ? ( -
+
{matchingMarkets.slice(0, 3).map((m) => (
- +{m.supplyFlowPct1h.toFixed(1)}% + = 0 ? 'text-green-500' : 'text-red-500'}`}> + {m.supplyFlowPct >= 0 ? '+' : ''}{m.supplyFlowPct.toFixed(1)}% +
))} - {totalMatches > 3 && +{totalMatches - 3} more} + {totalMatches > 3 && ( + +{totalMatches - 3} more + )}
) : ( No markets match current criteria diff --git a/src/stores/useMarketPreferences.ts b/src/stores/useMarketPreferences.ts index 43c22cec..3a6e9908 100644 --- a/src/stores/useMarketPreferences.ts +++ b/src/stores/useMarketPreferences.ts @@ -8,12 +8,10 @@ import { DEFAULT_COLUMN_VISIBILITY, type ColumnVisibility } from '@/features/mar export type FlowTimeWindow = '1h' | '24h' | '7d' | '30d'; export type CustomTagWindowConfig = { - // Supply flow thresholds (both must be met if set - AND logic) - minSupplyFlowPct: string; // e.g. "6" = 6% of current supply - minSupplyFlowUsd: string; // Absolute USD threshold - // Borrow flow thresholds (both must be met if set - AND logic) - minBorrowFlowPct: string; - minBorrowFlowUsd: string; + // Supply flow threshold (percentage, can be negative for outflows) + supplyFlowPct: string; // e.g. "5" = +5% growth, "-3" = -3% outflow + // Borrow flow threshold (percentage, can be negative) + borrowFlowPct: string; }; // Available icons for custom tags (react-icons identifiers) @@ -33,10 +31,10 @@ const DEFAULT_CUSTOM_TAG_CONFIG: CustomTagConfig = { enabled: false, icon: 'bookmark', windows: { - '1h': { minSupplyFlowPct: '6', minSupplyFlowUsd: '', minBorrowFlowPct: '', minBorrowFlowUsd: '' }, - '24h': { minSupplyFlowPct: '3', minSupplyFlowUsd: '', minBorrowFlowPct: '2', minBorrowFlowUsd: '' }, - '7d': { minSupplyFlowPct: '5', minSupplyFlowUsd: '', minBorrowFlowPct: '1', minBorrowFlowUsd: '' }, - '30d': { minSupplyFlowPct: '', minSupplyFlowUsd: '', minBorrowFlowPct: '', minBorrowFlowUsd: '' }, + '1h': { supplyFlowPct: '', borrowFlowPct: '' }, + '24h': { supplyFlowPct: '3', borrowFlowPct: '2' }, + '7d': { supplyFlowPct: '', borrowFlowPct: '' }, + '30d': { supplyFlowPct: '', borrowFlowPct: '' }, }, }; From 32fbae6be1af2ea7b55a1f31256baf96523e0333 Mon Sep 17 00:00:00 2001 From: anton Date: Thu, 29 Jan 2026 17:12:12 +0800 Subject: [PATCH 4/9] refactor: rename TrendingDetail to CustomTagDetail, remove 30d window, add GA tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename TrendingDetail.tsx → CustomTagDetail.tsx - Rename component TrendingDetail → CustomTagDetail - Change key 'trending-config' → 'custom-tag-config' - Remove 30d time window option (keep 1h, 24h, 7d) - Add GA event tracking for custom tag enable/disable --- src/components/shared/custom-tag-icons.tsx | 7 +++- .../markets/components/market-indicators.tsx | 21 ++++++++-- .../components/markets-filter-compact.tsx | 7 ++-- src/hooks/useFilteredMarkets.ts | 12 +++++- .../monarch-settings/SettingsContent.tsx | 4 +- .../settings/monarch-settings/constants.ts | 4 +- ...TrendingDetail.tsx => CustomTagDetail.tsx} | 42 ++++++++++--------- .../monarch-settings/details/index.ts | 2 +- .../panels/ExperimentalPanel.tsx | 2 +- src/stores/useMarketPreferences.ts | 8 +--- src/stores/useModalStore.ts | 2 +- 11 files changed, 70 insertions(+), 41 deletions(-) rename src/modals/settings/monarch-settings/details/{TrendingDetail.tsx => CustomTagDetail.tsx} (89%) diff --git a/src/components/shared/custom-tag-icons.tsx b/src/components/shared/custom-tag-icons.tsx index 0f9f6a24..404e35cd 100644 --- a/src/components/shared/custom-tag-icons.tsx +++ b/src/components/shared/custom-tag-icons.tsx @@ -35,7 +35,12 @@ type CustomTagIconProps = { export function CustomTagIcon({ iconId, size = 14, className = '' }: CustomTagIconProps) { const IconComponent = ICON_MAP[iconId]; if (!IconComponent) return null; - return ; + return ( + + ); } /** diff --git a/src/features/markets/components/market-indicators.tsx b/src/features/markets/components/market-indicators.tsx index 68b6d921..9643fd18 100644 --- a/src/features/markets/components/market-indicators.tsx +++ b/src/features/markets/components/market-indicators.tsx @@ -6,7 +6,13 @@ import { IoWarningOutline } from 'react-icons/io5'; import { AiOutlineFire } from 'react-icons/ai'; import { TooltipContent } from '@/components/shared/tooltip-content'; import { CustomTagIcon } from '@/components/shared/custom-tag-icons'; -import { useOfficialTrendingMarketKeys, useCustomTagMarketKeys, getMetricsKey, useEverLiquidated, useMarketMetricsMap } from '@/hooks/queries/useMarketMetricsQuery'; +import { + useOfficialTrendingMarketKeys, + useCustomTagMarketKeys, + getMetricsKey, + useEverLiquidated, + useMarketMetricsMap, +} from '@/hooks/queries/useMarketMetricsQuery'; import { computeMarketWarnings } from '@/hooks/useMarketWarnings'; import { useMarketPreferences } from '@/stores/useMarketPreferences'; import type { Market } from '@/utils/types'; @@ -143,14 +149,23 @@ export function MarketIndicators({ market, showRisk = false, isStared = false, h } + icon={ + + } title="Custom Tag" detail="Matches your custom tag criteria" /> } >
- +
)} diff --git a/src/features/positions/components/markets-filter-compact.tsx b/src/features/positions/components/markets-filter-compact.tsx index 23f4e330..365aea29 100644 --- a/src/features/positions/components/markets-filter-compact.tsx +++ b/src/features/positions/components/markets-filter-compact.tsx @@ -23,7 +23,7 @@ type MarketFilterProps = { variant?: 'ghost' | 'button'; }; -type DetailViewType = 'filter-thresholds' | 'trusted-vaults' | 'trending-config'; +type DetailViewType = 'filter-thresholds' | 'trusted-vaults' | 'custom-tag-config'; export function MarketFilter({ className, variant = 'ghost' }: MarketFilterProps) { const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure(); @@ -67,7 +67,8 @@ export function MarketFilter({ className, variant = 'ghost' }: MarketFilterProps }; const basicGuardianAllAllowed = includeUnknownTokens && showUnknownOracle && showUnwhitelistedMarkets && showLockedMarkets; - const advancedFilterActive = trustedVaultsOnly || minSupplyEnabled || minBorrowEnabled || minLiquidityEnabled || trendingMode || customTagMode; + const advancedFilterActive = + trustedVaultsOnly || minSupplyEnabled || minBorrowEnabled || minLiquidityEnabled || trendingMode || customTagMode; const hasActiveFilters = advancedFilterActive || !basicGuardianAllAllowed; const isButtonVariant = variant === 'button'; @@ -269,7 +270,7 @@ export function MarketFilter({ className, variant = 'ghost' }: MarketFilterProps