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
7 changes: 5 additions & 2 deletions .claude/skills/icons/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,12 @@ description: Icon usage conventions and semantic mappings. Use when adding, modi

| Concept | Icon | Import |
|---------|------|--------|
| Supply | `BsArrowUpCircle` | `react-icons/bs` |
| Deposit | `BsArrowUpCircle` | `react-icons/bs` |
| Borrow | `BsArrowDownLeftCircle` | `react-icons/bs` |
| Repay | `BsArrowUpRightCircle` | `react-icons/bs` |
| Withdraw | `BsArrowDownCircle` | `react-icons/bs` |
| Swap | `LuArrowRightLeft` | `react-icons/lu` |
| Withdraw | `BsArrowUpCircle` | `react-icons/bs` |
| Deposit | `BsArrowDownCircle` | `react-icons/bs` |
| History | `GoHistory` | `react-icons/go` |
| Rewards | `FiGift` | `react-icons/fi` |
| Fire | `HiFire` | `react-icons/hi2` |
Expand Down
7 changes: 7 additions & 0 deletions app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,13 @@
color-scheme: dark;
}

.dark .shadow-sm,
.dark .shadow-md,
.dark .shadow-lg {
box-shadow: none; /* Kill the shadow in dark mode */
border: 1px solid rgba(255, 255, 255, 0.08); /* Add a subtle white ring */
}

*,
*::before,
*::after {
Expand Down
28 changes: 14 additions & 14 deletions src/components/Info/info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import { IoWarningOutline } from 'react-icons/io5';
const levelToCellColor = (level: string) => {
switch (level) {
case 'info':
return 'bg-blue-200 text-blue-700';
return 'bg-blue-100 text-blue-800 dark:bg-blue-400/10 dark:text-blue-300';
case 'success':
return 'bg-green-200 text-green-700';
return 'bg-green-100 text-green-800 dark:bg-green-400/10 dark:text-green-300';
case 'warning':
return 'bg-yellow-200 text-yellow-700';
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-400/10 dark:text-yellow-300';
case 'alert':
return 'bg-red-200 text-red-700';
return 'bg-red-100 text-red-800 dark:bg-red-400/10 dark:text-red-300';
default:
return '';
}
Expand All @@ -23,33 +23,33 @@ const levelToIcon = (level: string) => {
case 'info':
return (
<FaRegLightbulb
className="mr-2"
size={18}
className="mt-0.5 flex-shrink-0"
size={16}
/>
);
case 'success':
return (
<GrStatusGood
className="mr-2"
size={18}
className="mt-0.5 flex-shrink-0"
size={16}
/>
);
case 'warning':
return (
<IoWarningOutline
className="mr-2"
size={18}
className="mt-0.5 flex-shrink-0"
size={16}
/>
);
case 'alert':
return (
<MdError
className="mr-2"
size={18}
className="mt-0.5 flex-shrink-0"
size={16}
/>
);
default:
return '';
return null;
}
};

Expand All @@ -60,7 +60,7 @@ const levelToIcon = (level: string) => {
*/
export function Info({ description, level, title }: { description: string; level: string; title?: string }) {
return (
<div className={`flex max-w-full items-start gap-2 rounded-sm ${levelToCellColor(level)} p-3 opacity-80`}>
<div className={`flex max-w-full items-start gap-2 rounded border border-current/20 ${levelToCellColor(level)} p-3`}>
{levelToIcon(level)}
<div className="min-w-0 flex-1">
{title && <h2 className="font-bold">{title}</h2>}
Expand Down
12 changes: 6 additions & 6 deletions src/components/layout/header/TransactionIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
'use client';

import { useCallback } from 'react';
import { FiDownload, FiRepeat } from 'react-icons/fi';
import { LuArrowRightLeft } from 'react-icons/lu';
import { BsArrowDownCircle, BsArrowUpCircle } from 'react-icons/bs';
import { BsArrowDownCircle, BsArrowDownLeftCircle, BsArrowUpCircle, BsArrowUpRightCircle } from 'react-icons/bs';
import { useShallow } from 'zustand/shallow';
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu';
import { useTransactionProcessStore } from '@/stores/useTransactionProcessStore';

const TX_TYPE_CONFIG: Record<string, { icon: React.ReactNode; label: string }> = {
supply: { icon: <BsArrowUpCircle className="h-4 w-4" />, label: 'Supply' },
borrow: { icon: <BsArrowDownCircle className="h-4 w-4" />, label: 'Borrow' },
repay: { icon: <FiRepeat className="h-4 w-4" />, label: 'Repay' },
vaultDeposit: { icon: <FiDownload className="h-4 w-4" />, label: 'Deposit' },
deposit: { icon: <FiDownload className="h-4 w-4" />, label: 'Deposit' },
borrow: { icon: <BsArrowDownLeftCircle className="h-4 w-4" />, label: 'Borrow' },
repay: { icon: <BsArrowUpRightCircle className="h-4 w-4" />, label: 'Repay' },
withdraw: { icon: <BsArrowDownCircle className="h-4 w-4" />, label: 'Withdraw' },
vaultDeposit: { icon: <BsArrowUpCircle className="h-4 w-4" />, label: 'Deposit' },
deposit: { icon: <BsArrowUpCircle className="h-4 w-4" />, label: 'Deposit' },
wrap: { icon: <LuArrowRightLeft className="h-4 w-4" />, label: 'Wrap' },
rebalance: { icon: <LuArrowRightLeft className="h-4 w-4" />, label: 'Rebalance' },
};
Expand Down
49 changes: 49 additions & 0 deletions src/components/shared/address-identity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Link from 'next/link';
import { ExternalLinkIcon } from '@radix-ui/react-icons';
import { Avatar } from '@/components/Avatar/Avatar';
import { TokenIcon } from '@/components/shared/token-icon';
import { getExplorerURL } from '@/utils/external';

type AddressIdentityProps = {
address: string;
chainId: number;
label?: string;
isToken?: boolean;
tokenSymbol?: string;
};
Comment thread
antoncoding marked this conversation as resolved.

/**
* Use to display address, not Account. Better used for contracts
* @param param0
* @returns
*/
export function AddressIdentity({ address, chainId, label, isToken, tokenSymbol }: AddressIdentityProps) {
return (
<Link
href={getExplorerURL(address as `0x${string}`, chainId)}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 rounded-sm bg-hovered px-2 py-1 text-xs text-secondary no-underline hover:bg-gray-300 dark:hover:bg-gray-700"
>
{isToken ? (
<TokenIcon
address={address}
chainId={chainId}
symbol={tokenSymbol ?? ''}
width={14}
height={14}
/>
) : (
<Avatar
address={address as `0x${string}`}
size={14}
/>
)}
{label && <span>{label}</span>}
<span className="font-mono text-[11px]">
{address.slice(0, 6)}...{address.slice(-4)}
</span>
<ExternalLinkIcon className="h-3 w-3" />
</Link>
);
}
4 changes: 3 additions & 1 deletion src/components/ui/button-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type ButtonGroupProps = {
onChange: (value: ButtonOption['value']) => void;
size?: 'sm' | 'md' | 'lg';
variant?: 'default' | 'primary';
equalWidth?: boolean;
};

const sizeClasses = {
Expand All @@ -41,7 +42,7 @@ const variantStyles = {
],
};

export default function ButtonGroup({ options, value, onChange, size = 'md', variant = 'default' }: ButtonGroupProps) {
export default function ButtonGroup({ options, value, onChange, size = 'md', variant = 'default', equalWidth = false }: ButtonGroupProps) {
return (
<div
className="inline-flex rounded shadow-sm"
Expand All @@ -62,6 +63,7 @@ export default function ButtonGroup({ options, value, onChange, size = 'md', var
// Base styles
'relative font-medium transition-all duration-200',
sizeClasses[size],
equalWidth && 'min-w-[3rem] text-center',

// Position-based styles
isFirst ? 'rounded-l' : '-ml-px rounded-none',
Expand Down
24 changes: 19 additions & 5 deletions src/features/market-detail/components/campaign-badge.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useState } from 'react';
import { useState, useMemo } from 'react';
import { FaGift } from 'react-icons/fa';
import { Badge } from '@/components/ui/badge';
import { useMarketCampaigns } from '@/hooks/useMarketCampaigns';
Expand All @@ -11,9 +11,15 @@ type CampaignBadgeProps = {
loanTokenAddress: string;
chainId: number;
whitelisted: boolean;
filterType?: 'supply' | 'borrow';
};

export function CampaignBadge({ marketId, loanTokenAddress, chainId, whitelisted }: CampaignBadgeProps) {
// Supply campaign types: MORPHOSUPPLY, MORPHOSUPPLY_SINGLETOKEN, MULTILENDBORROW
const SUPPLY_CAMPAIGN_TYPES = ['MORPHOSUPPLY', 'MORPHOSUPPLY_SINGLETOKEN', 'MULTILENDBORROW'];
// Borrow campaign types: MORPHOBORROW
const BORROW_CAMPAIGN_TYPES = ['MORPHOBORROW'];

export function CampaignBadge({ marketId, loanTokenAddress, chainId, whitelisted, filterType }: CampaignBadgeProps) {
const [isModalOpen, setIsModalOpen] = useState(false);

const { activeCampaigns, hasActiveRewards, loading } = useMarketCampaigns({
Expand All @@ -23,11 +29,19 @@ export function CampaignBadge({ marketId, loanTokenAddress, chainId, whitelisted
whitelisted,
});

if (loading || !hasActiveRewards) {
// Filter campaigns by type if filterType is specified
const filteredCampaigns = useMemo(() => {
if (!filterType) return activeCampaigns;

const allowedTypes = filterType === 'supply' ? SUPPLY_CAMPAIGN_TYPES : BORROW_CAMPAIGN_TYPES;
return activeCampaigns.filter((campaign) => allowedTypes.includes(campaign.type));
}, [activeCampaigns, filterType]);

if (loading || !hasActiveRewards || filteredCampaigns.length === 0) {
return null;
}

const totalBonus = activeCampaigns.reduce((sum, campaign) => sum + campaign.apr, 0);
const totalBonus = filteredCampaigns.reduce((sum, campaign) => sum + campaign.apr, 0);

return (
<>
Expand All @@ -52,7 +66,7 @@ export function CampaignBadge({ marketId, loanTokenAddress, chainId, whitelisted
<CampaignModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
campaigns={activeCampaigns}
campaigns={filteredCampaigns}
/>
</>
);
Expand Down
125 changes: 125 additions & 0 deletions src/features/market-detail/components/charts/chart-utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import type { Dispatch, SetStateAction } from 'react';
import { CHART_COLORS } from '@/constants/chartColors';

export const TIMEFRAME_LABELS: Record<string, string> = {
'1d': '1D',
'7d': '7D',
'30d': '30D',
};

type GradientConfig = {
id: string;
color: string;
};

export function ChartGradients({ prefix, gradients }: { prefix: string; gradients: GradientConfig[] }) {
return (
<defs>
{gradients.map(({ id, color }) => (
<linearGradient
key={id}
id={`${prefix}-${id}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor={color}
stopOpacity={0.08}
/>
<stop
offset="100%"
stopColor={color}
stopOpacity={0}
/>
</linearGradient>
))}
</defs>
);
}

export const RATE_CHART_GRADIENTS: GradientConfig[] = [
{ id: 'supplyGradient', color: CHART_COLORS.supply.stroke },
{ id: 'borrowGradient', color: CHART_COLORS.borrow.stroke },
{ id: 'targetGradient', color: CHART_COLORS.apyAtTarget.stroke },
];

export const VOLUME_CHART_GRADIENTS: GradientConfig[] = [
{ id: 'supplyGradient', color: CHART_COLORS.supply.stroke },
{ id: 'borrowGradient', color: CHART_COLORS.borrow.stroke },
{ id: 'liquidityGradient', color: CHART_COLORS.apyAtTarget.stroke },
];

type ChartTooltipContentProps = {
active?: boolean;
payload?: any[];
label?: number;
formatValue: (value: number) => string;
};

export function ChartTooltipContent({ active, payload, label, formatValue }: ChartTooltipContentProps) {
if (!active || !payload) return null;
return (
<div className="rounded-lg border border-border bg-background p-3 shadow-lg">
<p className="mb-2 text-xs text-secondary">{new Date((label ?? 0) * 1000).toLocaleDateString()}</p>
<div className="space-y-1">
{payload.map((entry: any) => (
<div
key={entry.dataKey}
className="flex items-center justify-between gap-6 text-sm"
>
<div className="flex items-center gap-2">
<span
className="h-2 w-2 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<span className="text-secondary">{entry.name}</span>
</div>
<span className="tabular-nums">{formatValue(entry.value)}</span>
</div>
))}
</div>
</div>
);
}

type ChartLegendProps<T extends Record<string, boolean>> = {
visibleLines: T;
setVisibleLines: Dispatch<SetStateAction<T>>;
};

export function createLegendClickHandler<T extends Record<string, boolean>>({ visibleLines, setVisibleLines }: ChartLegendProps<T>) {
return {
onClick: (e: any) => {
const dataKey = e.dataKey as keyof T;
setVisibleLines((prev) => ({
...prev,
[dataKey]: !prev[dataKey],
}));
},
formatter: (value: string, entry: any) => (
<span
className="text-xs"
style={{
color: visibleLines[entry.dataKey as keyof T] ? 'var(--color-text-secondary)' : '#666',
}}
>
{value}
</span>
),
};
}

export const chartTooltipCursor = {
stroke: 'var(--color-text-secondary)',
strokeWidth: 1,
strokeDasharray: '4 4',
};

export const chartLegendStyle = {
wrapperStyle: { fontSize: '12px', paddingTop: '8px' },
iconType: 'circle' as const,
iconSize: 8,
};
Loading