Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions src/components/shared/custom-tag-icons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use client';

import type { IconType } from 'react-icons';
import { AiOutlineThunderbolt, AiOutlineEye, AiOutlineRocket } from 'react-icons/ai';
import { FaGem } from 'react-icons/fa';
import { TbTrendingUp, TbTrendingDown } from 'react-icons/tb';
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<CustomTagIconId, IconType> = {
'trend-up': TbTrendingUp,
'trend-down': TbTrendingDown,
rocket: AiOutlineRocket,
gem: FaGem,
bolt: AiOutlineThunderbolt,
eye: AiOutlineEye,
};

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 (
<IconComponent
size={size}
className={className}
/>
);
}

/**
* 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 (
<div className="flex flex-wrap gap-1.5">
{(Object.keys(ICON_MAP) as CustomTagIconId[]).map((iconId) => {
const IconComponent = ICON_MAP[iconId];
const isSelected = iconId === selectedIcon;

return (
<button
key={iconId}
type="button"
onClick={() => onSelect(iconId)}
disabled={disabled}
aria-pressed={isSelected}
aria-label={`Select ${iconId} icon`}
className={`flex h-8 w-8 items-center justify-center rounded-md border transition-all ${
isSelected
? '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}
>
<IconComponent size={16} />
</button>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);
})}
</div>
);
}
114 changes: 105 additions & 9 deletions src/features/markets/components/market-indicators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,66 @@ 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,
type FlowTimeWindow,
} from '@/hooks/queries/useMarketMetricsQuery';
import { computeMarketWarnings } from '@/hooks/useMarketWarnings';
import { useMarketPreferences } from '@/stores/useMarketPreferences';
import { useMarketPreferences, type CustomTagConfig } from '@/stores/useMarketPreferences';
import type { Market } from '@/utils/types';
import { RewardsIndicator } from '@/features/markets/components/rewards-indicator';

const ICON_SIZE = 14;

const WINDOW_LABELS: Record<FlowTimeWindow, string> = {
'1h': '1 Hour',
'24h': '24 Hours',
'7d': '7 Days',
};

/**
* Build tooltip detail showing actual flow values for configured thresholds
*/
function buildCustomTagDetail(
config: CustomTagConfig,
flows: Record<FlowTimeWindow, { supplyFlowPct?: number; borrowFlowUsd?: number }> | undefined,
borrowUsd: number,
): string {
if (!flows) return 'Matches your custom tag criteria';

const parts: string[] = [];

for (const [window, windowConfig] of Object.entries(config.windows)) {
const supplyThreshold = windowConfig?.supplyFlowPct ?? '';
const borrowThreshold = windowConfig?.borrowFlowPct ?? '';

if (!supplyThreshold && !borrowThreshold) continue;

const flow = flows[window as FlowTimeWindow];
if (!flow) continue;

const label = WINDOW_LABELS[window as FlowTimeWindow] ?? window;
const actualSupply = flow.supplyFlowPct ?? 0;
const actualBorrow = borrowUsd > 0 ? ((flow.borrowFlowUsd ?? 0) / borrowUsd) * 100 : 0;

if (supplyThreshold && Number.isFinite(Number(supplyThreshold))) {
const sign = actualSupply >= 0 ? '+' : '';
parts.push(`${label}: ${sign}${actualSupply.toFixed(1)}% supply`);
}
if (borrowThreshold && Number.isFinite(Number(borrowThreshold))) {
const sign = actualBorrow >= 0 ? '+' : '';
parts.push(`${label}: ${sign}${actualBorrow.toFixed(1)}% borrow`);
}
}

return parts.length > 0 ? parts.join('\n') : 'Matches your custom tag criteria';
}

type MarketIndicatorsProps = {
market: Market;
showRisk?: boolean;
Expand All @@ -22,9 +74,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');
Expand Down Expand Up @@ -101,29 +164,62 @@ export function MarketIndicators({ market, showRisk = false, isStared = false, h
whitelisted={market.whitelisted}
/>

{isTrending && (
{/* Official Trending (backend-computed) */}
{isOfficialTrending && (
<Tooltip
content={
<TooltipContent
icon={
<AiOutlineFire
size={ICON_SIZE + 2}
size={ICON_SIZE}
className="text-orange-500"
/>
}
detail="This market is trending based on flow metrics"
title="Trending"
detail={trendingReason ?? 'This market is trending based on flow activity'}
/>
}
>
<div className="flex-shrink-0">
<AiOutlineFire
size={ICON_SIZE + 2}
size={ICON_SIZE}
className="text-orange-500"
/>
</div>
</Tooltip>
)}

{/* User's Custom Tag */}
{hasCustomTag &&
(() => {
const metrics = metricsMap.get(marketKey);
const tooltipDetail = buildCustomTagDetail(customTagConfig, metrics?.flows, metrics?.currentState?.borrowUsd ?? 0);
return (
<Tooltip
content={
<TooltipContent
icon={
<CustomTagIcon
iconId={customTagConfig.icon}
size={ICON_SIZE}
className="text-primary"
/>
}
title="Custom Tag"
detail={tooltipDetail}
/>
}
>
<div className="flex-shrink-0 text-primary">
<CustomTagIcon
iconId={customTagConfig.icon}
size={ICON_SIZE}
/>
</div>
</Tooltip>
);
})()}

{showRisk && hasWarnings && (
<Tooltip
content={
Expand Down
37 changes: 27 additions & 10 deletions src/features/positions/components/markets-filter-compact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -48,10 +48,11 @@ export function MarketFilter({ className, variant = 'ghost' }: MarketFilterProps
usdMinSupply,
usdMinBorrow,
usdMinLiquidity,
trendingConfig,
showOfficialTrending,
customTagConfig,
} = useMarketPreferences();

const { trendingMode, toggleTrendingMode } = useMarketsFilters();
const { trendingMode, toggleTrendingMode, customTagMode, toggleCustomTagMode } = useMarketsFilters();
const { showUnwhitelistedMarkets, setShowUnwhitelistedMarkets } = useAppSettings();
const { vaults: trustedVaults } = useTrustedVaults();
const trustedVaultCount = trustedVaults.length;
Expand All @@ -66,7 +67,8 @@ export function MarketFilter({ className, variant = 'ghost' }: MarketFilterProps
};

const basicGuardianAllAllowed = includeUnknownTokens && showUnknownOracle && showUnwhitelistedMarkets && showLockedMarkets;
const advancedFilterActive = trustedVaultsOnly || minSupplyEnabled || minBorrowEnabled || minLiquidityEnabled || trendingMode;
const advancedFilterActive =
trustedVaultsOnly || minSupplyEnabled || minBorrowEnabled || minLiquidityEnabled || trendingMode || customTagMode;
const hasActiveFilters = advancedFilterActive || !basicGuardianAllAllowed;
Comment on lines 69 to 72
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Gate advancedFilterActive by feature availability.
Otherwise the filter indicator can show β€œactive” while the toggle is hidden.

Proposed fix
-  const advancedFilterActive = trustedVaultsOnly || minSupplyEnabled || minBorrowEnabled || minLiquidityEnabled || trendingMode || customTagMode;
+  const advancedFilterActive =
+    trustedVaultsOnly ||
+    minSupplyEnabled ||
+    minBorrowEnabled ||
+    minLiquidityEnabled ||
+    (showOfficialTrending && trendingMode) ||
+    (customTagConfig.enabled && customTagMode);
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const basicGuardianAllAllowed = includeUnknownTokens && showUnknownOracle && showUnwhitelistedMarkets && showLockedMarkets;
const advancedFilterActive = trustedVaultsOnly || minSupplyEnabled || minBorrowEnabled || minLiquidityEnabled || trendingMode;
const advancedFilterActive = trustedVaultsOnly || minSupplyEnabled || minBorrowEnabled || minLiquidityEnabled || trendingMode || customTagMode;
const hasActiveFilters = advancedFilterActive || !basicGuardianAllAllowed;
const basicGuardianAllAllowed = includeUnknownTokens && showUnknownOracle && showUnwhitelistedMarkets && showLockedMarkets;
const advancedFilterActive =
trustedVaultsOnly ||
minSupplyEnabled ||
minBorrowEnabled ||
minLiquidityEnabled ||
(showOfficialTrending && trendingMode) ||
(customTagConfig.enabled && customTagMode);
const hasActiveFilters = advancedFilterActive || !basicGuardianAllAllowed;
πŸ€– Prompt for AI Agents
In `@src/features/positions/components/markets-filter-compact.tsx` around lines 69
- 71, The advancedFilterActive boolean must be gated by the feature/visibility
flag so the active-indicator cannot be true when the advanced toggle is hidden;
change the definition of advancedFilterActive to include the availability check
(e.g., const advancedFilterActive = advancedFiltersAvailable &&
(trustedVaultsOnly || minSupplyEnabled || minBorrowEnabled ||
minLiquidityEnabled || trendingMode || customTagMode)) and then keep
hasActiveFilters = advancedFilterActive || !basicGuardianAllAllowed; ensure
advancedFiltersAvailable is derived from the existing feature flag or UI prop
that controls showing the advanced toggle.


const isButtonVariant = variant === 'button';
Expand Down Expand Up @@ -244,24 +246,39 @@ export function MarketFilter({ className, variant = 'ghost' }: MarketFilterProps
/>
</div>
</FilterRow>
{trendingConfig.enabled && (
{/* Official Trending Filter (backend-computed) */}
{showOfficialTrending && (
<FilterRow
title="Trending Only"
description="Show markets matching your trending criteria"
description="Show officially trending markets"
>
<IconSwitch
selected={trendingMode}
onChange={toggleTrendingMode}
size="xs"
color="primary"
/>
</FilterRow>
)}
{/* Custom Tag Filter (user-defined) */}
{customTagConfig.enabled && (
<FilterRow
title="Custom Tag Only"
description="Show markets matching your custom tag"
>
<div className="flex items-center gap-1.5">
<Button
variant="ghost"
size="icon"
onClick={() => handleOpenDetailView('trending-config')}
aria-label="Configure trending"
onClick={() => handleOpenDetailView('custom-tag-config')}
aria-label="Configure custom tag"
className="h-6 w-6"
>
<GoGear className="h-3.5 w-3.5" />
</Button>
<IconSwitch
selected={trendingMode}
onChange={toggleTrendingMode}
selected={customTagMode}
onChange={toggleCustomTagMode}
size="xs"
color="primary"
/>
Expand Down
Loading