
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Nodeka and Uniswap-inspired decentralized finance staking yield vault, liquidity pool APY calculator, and reward claimer with live TVL metrics, multi-tier lockup multipliers, and real-time yield simulator.
Also available for Vue ->$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/defi-staking-yield-vault.json$npx shadcn@latest add https://uipkge.dev/r/react/defi-staking-yield-vault.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/defi-staking-yield-vault.json$bunx shadcn@latest add https://uipkge.dev/r/react/defi-staking-yield-vault.jsonnpx shadcn@latest add @uipkge-react/defi-staking-yield-vaultInstalls to:components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
symbol | string | — | required |
name | string | — | required |
price | number | — | required |
balance | number | — | required |
apyBoost | number | — | required |
description | string | — | required |
iconColor | string | — | required |
Type aliases exported from this item's source. Use these to shape the data you pass in.
TokenOptioninterface TokenOption {
symbol: string
name: string
price: number
balance: number
apyBoost: number
description: string
iconColor: string
}LockupOptioninterface LockupOption {
id: string
label: string
durationText: string
days: number
boost: number
badge: string
}VaultIteminterface VaultItem {
id: string
name: string
pairSymbol: string
protocol: string
apy: string
apyRaw: number
tvl: string
risk: 'Low Risk' | 'Medium Risk' | 'High Risk'
riskVariant: 'emerald' | 'amber' | 'rose'
strategy: string
metricLabel: string
metricValue: string
isCurrentVault?: boolean
}'use client'
import * as React from 'react'
import {
ArrowUpRight,
Check,
Coins,
Copy,
Flame,
Fuel,
Lock,
Percent,
RefreshCw,
ShieldAlert,
ShieldCheck,
TrendingUp,
Unlock,
Wallet,
Zap,
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Separator } from '@/components/ui/separator'
import { Slider } from '@/components/ui/slider'
export interface DefiStakingYieldVaultProps extends React.HTMLAttributes<HTMLDivElement> {}
interface TokenOption {
symbol: string
name: string
price: number
balance: number
apyBoost: number
description: string
iconColor: string
}
const tokenOptions: TokenOption[] = [
{
symbol: 'ETH',
name: 'Native Ether',
price: 3450.0,
balance: 32.5,
apyBoost: 0,
description: 'Base Layer Staking',
iconColor: '#627EEA',
},
{
symbol: 'stETH',
name: 'Lido Staked ETH',
price: 3450.0,
balance: 15.2,
apyBoost: 0,
description: '1:1 Rebase Token',
iconColor: '#00A3FF',
},
{
symbol: 'wstETH',
name: 'Wrapped stETH',
price: 4015.0,
balance: 8.4,
apyBoost: 0.15,
description: 'Compounding Value Token',
iconColor: '#0052FF',
},
]
interface LockupOption {
id: string
label: string
durationText: string
days: number
boost: number
badge: string
}
const lockupOptions: LockupOption[] = [
{ id: 'flex', label: 'Flexible', durationText: '0-Day Lock', days: 0, boost: 0, badge: 'Flexible' },
{ id: '30d', label: '30 Days', durationText: '30-Day Lock', days: 30, boost: 0.5, badge: '+0.5% APY' },
{ id: '90d', label: '90 Days', durationText: '90-Day Lock', days: 90, boost: 1.2, badge: '+1.2% APY' },
{ id: '365d', label: '365 Days', durationText: '365-Day Lock', days: 365, boost: 2.5, badge: '+2.5% APY' },
]
interface VaultItem {
id: string
name: string
pairSymbol: string
protocol: string
apy: string
apyRaw: number
tvl: string
risk: 'Low Risk' | 'Medium Risk' | 'High Risk'
riskVariant: 'emerald' | 'amber' | 'rose'
strategy: string
metricLabel: string
metricValue: string
isCurrentVault?: boolean
}
const yieldVaults: VaultItem[] = [
{
id: 'eth-usdc-lp',
name: 'ETH-USDC LP',
pairSymbol: 'ETH + USDC',
protocol: 'Uniswap v3 · Concentrated',
apy: '12.40%',
apyRaw: 12.4,
tvl: '$58,420,000.00',
risk: 'Medium Risk',
riskVariant: 'amber',
strategy: 'Automated Rebalancing Range ±5%',
metricLabel: '24h Volume',
metricValue: '$4.2M Vol',
isCurrentVault: false,
},
{
id: 'liquid-staked-eth',
name: 'Liquid Staked ETH',
pairSymbol: 'ETH / stETH',
protocol: 'Lido & Distributed Validator',
apy: '7.42%',
apyRaw: 7.42,
tvl: '$142,850,000.00',
risk: 'Low Risk',
riskVariant: 'emerald',
strategy: 'Consensus Layer + MEV Boost',
metricLabel: 'Lockup Period',
metricValue: 'Instant 0-Day',
isCurrentVault: true,
},
{
id: 'stablecoin-vault',
name: 'Stablecoin Multi-Vault',
pairSymbol: 'USDC + USDT + DAI',
protocol: 'Curve & Morpho Blue Optimizer',
apy: '9.80%',
apyRaw: 9.8,
tvl: '$94,180,000.00',
risk: 'Low Risk',
riskVariant: 'emerald',
strategy: 'Multi-Asset Lending & Arbitrage Yield',
metricLabel: 'Coverage',
metricValue: 'Nexus Mutual Protected',
isCurrentVault: false,
},
]
export function DefiStakingYieldVault({ className, ...props }: DefiStakingYieldVaultProps) {
// Wallet State
const [isWalletConnected, setIsWalletConnected] = React.useState(true)
const walletAddress = '0x71C856402244243b92834b9d09c2534575823a9F'
const shortAddress = '0x71C...3a9F'
const [copied, setCopied] = React.useState(false)
const copyAddress = React.useCallback(() => {
if (typeof navigator !== 'undefined' && navigator.clipboard) {
navigator.clipboard.writeText(walletAddress)
setCopied(true)
setTimeout(() => {
setCopied(false)
}, 2000)
}
}, [walletAddress])
// Rewards State
const [unclaimedEth, setUnclaimedEth] = React.useState(0.485)
const [isClaiming, setIsClaiming] = React.useState(false)
const [claimSuccess, setClaimSuccess] = React.useState(false)
const unclaimedUsd = React.useMemo(() => {
return (unclaimedEth * 3450).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}, [unclaimedEth])
const handleClaimRewards = () => {
if (unclaimedEth <= 0 || isClaiming) return
setIsClaiming(true)
setTimeout(() => {
setIsClaiming(false)
setClaimSuccess(true)
setUnclaimedEth(0)
setTimeout(() => {
setClaimSuccess(false)
}, 3500)
}, 1200)
}
// Simulator State
const [selectedTokenSymbol, setSelectedTokenSymbol] = React.useState('ETH')
const [selectedLockupId, setSelectedLockupId] = React.useState('flex')
const [stakeAmountStr, setStakeAmountStr] = React.useState('10.0')
const [isStaking, setIsStaking] = React.useState(false)
const [stakeSuccess, setStakeSuccess] = React.useState(false)
const currentToken = React.useMemo(() => {
return tokenOptions.find((t) => t.symbol === selectedTokenSymbol) ?? tokenOptions[0]
}, [selectedTokenSymbol])
const currentLockup = React.useMemo(() => {
return lockupOptions.find((l) => l.id === selectedLockupId) ?? lockupOptions[0]
}, [selectedLockupId])
const parsedAmount = React.useMemo(() => {
const val = parseFloat(stakeAmountStr)
return isNaN(val) || val < 0 ? 0 : val
}, [stakeAmountStr])
const baseVaultApy = 7.42
const effectiveApy = React.useMemo(() => {
const total = baseVaultApy + currentLockup.boost + currentToken.apyBoost
return total.toFixed(2)
}, [currentLockup.boost, currentToken.apyBoost])
const stakedFiatValue = React.useMemo(() => {
return (parsedAmount * currentToken.price).toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
}, [parsedAmount, currentToken.price])
const annualYieldEth = React.useMemo(() => {
const rate = parseFloat(effectiveApy) / 100
return (parsedAmount * rate).toFixed(3)
}, [effectiveApy, parsedAmount])
const annualYieldUsd = React.useMemo(() => {
const rate = parseFloat(effectiveApy) / 100
const usd = parsedAmount * rate * currentToken.price
return usd.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
}, [effectiveApy, parsedAmount, currentToken.price])
const monthlyYieldUsd = React.useMemo(() => {
const rate = parseFloat(effectiveApy) / 100
const usd = (parsedAmount * rate * currentToken.price) / 12
return usd.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
}, [effectiveApy, parsedAmount, currentToken.price])
const monthlyYieldEth = React.useMemo(() => {
const rate = parseFloat(effectiveApy) / 100
return ((parsedAmount * rate) / 12).toFixed(4)
}, [effectiveApy, parsedAmount])
const dailyYieldUsd = React.useMemo(() => {
const rate = parseFloat(effectiveApy) / 100
const usd = (parsedAmount * rate * currentToken.price) / 365
return usd.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
}, [effectiveApy, parsedAmount, currentToken.price])
const dailyYieldEth = React.useMemo(() => {
const rate = parseFloat(effectiveApy) / 100
return ((parsedAmount * rate) / 365).toFixed(5)
}, [effectiveApy, parsedAmount])
const handleSliderChange = (vals: number[]) => {
if (vals && vals[0] !== undefined) {
setStakeAmountStr(vals[0].toFixed(2))
}
}
const setPercentageAmount = (pct: number) => {
const amt = currentToken.balance * (pct / 100)
setStakeAmountStr(amt.toFixed(2))
}
const setMaxAmount = () => {
setStakeAmountStr(currentToken.balance.toFixed(2))
}
const handleExecuteStake = () => {
if (parsedAmount <= 0 || isStaking) return
setIsStaking(true)
setTimeout(() => {
setIsStaking(false)
setStakeSuccess(true)
setTimeout(() => {
setStakeSuccess(false)
}, 3500)
}, 1400)
}
return (
<div
data-slot="defi-staking-yield-vault"
className={cn('mx-auto w-full max-w-6xl space-y-6', className)}
{...props}
>
{/* Header Section */}
<header className="border-border/80 bg-card/60 rounded-xl border p-4 shadow-xs backdrop-blur-xs sm:p-6">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
{/* Title & Protocol Badges */}
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-2.5">
<div
className="bg-primary/10 text-primary border-primary/20 flex size-9 shrink-0 items-center justify-center rounded-lg border shadow-xs"
aria-hidden="true"
>
<Zap className="size-5" />
</div>
<div>
<div className="flex flex-wrap items-center gap-2">
<h1 className="text-foreground text-xl font-bold tracking-tight sm:text-2xl">
ETH Liquid Staking & Yield Vault v3
</h1>
<Badge variant="outline" className="border-border/80 font-mono text-xs font-normal">
v3.2 Protocol
</Badge>
</div>
</div>
</div>
<div className="flex flex-wrap items-center gap-2.5 text-xs">
{/* Audit Badge */}
<Badge variant="outline" className="border-success/30 bg-success/10 text-success gap-1.5 font-normal">
<ShieldCheck className="size-3.5" aria-hidden="true" />
Audited by Trail of Bits & OpenZeppelin
</Badge>
{/* TVL Badge */}
<Badge variant="secondary" className="gap-1.5 font-medium tabular-nums">
<span className="bg-success size-1.5 animate-pulse rounded-full" aria-hidden="true" />
$142,850,000.00 TVL
</Badge>
<span className="text-muted-foreground hidden items-center gap-1 sm:inline-flex">
<Zap className="text-warning size-3" aria-hidden="true" />
Auto-Compounding Enabled
</span>
</div>
</div>
{/* Wallet Connection Action */}
<div className="flex flex-wrap items-center gap-2.5">
{isWalletConnected ? (
<>
<div className="border-border bg-muted/60 flex items-center gap-2 rounded-lg border px-3 py-1.5 shadow-xs">
<span className="bg-success size-2 rounded-full" aria-hidden="true" />
<span className="text-foreground font-mono text-xs font-medium">{shortAddress}</span>
<Button
variant="ghost"
size="icon-sm"
className="text-muted-foreground hover:text-foreground size-5"
aria-label="Copy wallet address"
onClick={copyAddress}
>
{copied ? (
<Check className="text-success size-3" aria-hidden="true" />
) : (
<Copy className="size-3" aria-hidden="true" />
)}
</Button>
</div>
<Button
variant="outline"
size="sm"
className="shadow-xs active:scale-[0.98]"
onClick={() => setIsWalletConnected(false)}
>
Disconnect
</Button>
</>
) : (
<Button
size="sm"
className="gap-2 shadow-xs active:scale-[0.98]"
onClick={() => setIsWalletConnected(true)}
>
<Wallet className="size-4" aria-hidden="true" />
Connect Web3 Wallet
</Button>
)}
</div>
</div>
</header>
{/* 4 Staking Performance KPI Cards */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{/* Card 1: Net Staking APY */}
<Card className="border-border/80 shadow-xs">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardDescription className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
Net Staking APY
</CardDescription>
<div
className="border-success/30 bg-success/10 text-success flex size-7 items-center justify-center rounded-md border"
aria-hidden="true"
>
<TrendingUp className="size-4" />
</div>
</div>
<div className="mt-1 flex items-baseline gap-2">
<span className="text-success text-success text-2xl font-bold tracking-tight tabular-nums sm:text-3xl">
7.42% APY
</span>
</div>
</CardHeader>
<CardContent className="text-muted-foreground space-y-1 text-xs">
<div className="text-success flex items-center gap-1.5 font-medium">
<span className="bg-success size-1.5 rounded-full" aria-hidden="true" />
Auto-Compounding daily
</div>
<p className="text-muted-foreground tabular-nums">Base 4.92% + 2.50% MEV Boost</p>
</CardContent>
</Card>
{/* Card 2: Your Staked Balance */}
<Card className="border-border/80 shadow-xs">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardDescription className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
Your Staked Balance
</CardDescription>
<div
className="border-primary/30 bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md border"
aria-hidden="true"
>
<Coins className="size-4" />
</div>
</div>
<div className="mt-1 flex items-baseline gap-2">
<span className="text-foreground text-2xl font-bold tracking-tight tabular-nums sm:text-3xl">
24.50 ETH
</span>
</div>
</CardHeader>
<CardContent className="text-muted-foreground space-y-1 text-xs">
<div className="text-muted-foreground font-medium tabular-nums">≈ $84,525.00 USD</div>
<div className="flex items-center gap-1">
<Badge variant="outline" className="border-border/80 px-1.5 py-0 text-xs font-normal">
Active in Vault v3
</Badge>
</div>
</CardContent>
</Card>
{/* Card 3: Unclaimed Rewards */}
<Card className="border-border/80 shadow-xs">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardDescription className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
Unclaimed Rewards
</CardDescription>
<div
className="border-warning/30 bg-warning/10 text-warning flex size-7 items-center justify-center rounded-md border"
aria-hidden="true"
>
<Coins className="size-4" />
</div>
</div>
<div className="mt-1 flex items-baseline gap-2">
<span className="text-foreground text-2xl font-bold tracking-tight tabular-nums sm:text-3xl">
{unclaimedEth} stETH
</span>
</div>
</CardHeader>
<CardContent className="space-y-2.5 text-xs">
<div className="text-muted-foreground font-medium tabular-nums">≈ ${unclaimedUsd} USD</div>
<Button
variant="outline"
size="sm"
className="h-7 w-full gap-1.5 text-xs font-medium shadow-xs"
disabled={unclaimedEth <= 0 || isClaiming}
onClick={handleClaimRewards}
>
{isClaiming ? (
<RefreshCw className="size-3 animate-spin" aria-hidden="true" />
) : claimSuccess ? (
<Check className="text-success size-3" aria-hidden="true" />
) : null}
{isClaiming ? 'Claiming On-Chain...' : claimSuccess ? 'Rewards Claimed!' : 'Claim Rewards'}
</Button>
</CardContent>
</Card>
{/* Card 4: Staking Pool Share */}
<Card className="border-border/80 shadow-xs">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardDescription className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
Staking Pool Share
</CardDescription>
<div
className="border-chart-1/30 bg-chart-1/10 text-chart-1 flex size-7 items-center justify-center rounded-md border"
aria-hidden="true"
>
<Percent className="size-4" />
</div>
</div>
<div className="mt-1 flex items-baseline gap-2">
<span className="text-foreground text-2xl font-bold tracking-tight tabular-nums sm:text-3xl">0.017%</span>
</div>
</CardHeader>
<CardContent className="text-muted-foreground space-y-2 text-xs">
<div className="flex items-center justify-between">
<span>of total vault</span>
<span className="text-foreground font-medium tabular-nums">$142.85M TVL</span>
</div>
<div className="bg-muted/80 h-1.5 w-full overflow-hidden rounded-full">
<div className="bg-primary h-full w-[17%] rounded-full" />
</div>
</CardContent>
</Card>
</div>
{/* 2-Column Staking Simulator & Yield Directory */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-12">
{/* Left Column: Deposit & Stake Calculator (7 Cols) */}
<div className="space-y-6 lg:col-span-7">
<Card className="border-border/80 shadow-xs">
<CardHeader className="pb-4">
<div className="flex items-center justify-between">
<div className="space-y-1">
<CardTitle className="text-foreground flex items-center gap-2 text-base font-semibold sm:text-lg">
<Zap className="text-primary size-5" aria-hidden="true" />
Deposit & Stake Calculator
</CardTitle>
<CardDescription className="text-muted-foreground text-xs">
Simulate staking yields with custom lockup multipliers and real-time auto-compounding.
</CardDescription>
</div>
<Badge variant="outline" className="border-success/30 bg-success/10 text-success text-xs font-semibold">
{effectiveApy}% APY
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-5">
{/* Token Selector Tabs */}
<div className="space-y-2">
<div className="text-muted-foreground flex items-center justify-between text-xs">
<label className="text-foreground font-medium">Select Staking Asset</label>
<span className="tabular-nums">
Available: {currentToken.balance} {currentToken.symbol}
</span>
</div>
<div className="grid grid-cols-3 gap-2">
{tokenOptions.map((token) => {
const isSelected = selectedTokenSymbol === token.symbol
return (
<button
key={token.symbol}
type="button"
className={cn(
'border-border/80 focus-visible:ring-ring flex flex-col items-start gap-1 rounded-lg border p-3 text-left transition-[colors,transform] duration-150 focus-visible:ring-2 focus-visible:outline-none active:scale-[0.98]',
isSelected
? 'border-primary bg-primary/5 ring-primary/20 shadow-xs ring-1'
: 'bg-card hover:bg-muted/50 text-muted-foreground',
)}
onClick={() => setSelectedTokenSymbol(token.symbol)}
>
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-2">
{token.symbol === 'ETH' && (
<svg
viewBox="0 0 32 32"
className="size-5 shrink-0 rounded-full shadow-xs"
aria-hidden="true"
>
<circle cx="16" cy="16" r="16" fill="#627EEA" />
<path fill="#ffffff" fillOpacity="0.6" d="M16.5 4v8.87l7.5 3.35z" />
<path fill="#ffffff" d="M16.5 4L9 16.22l7.5-3.35z" />
<path fill="#ffffff" fillOpacity="0.6" d="M16.5 21.97v6.03L24 17.62z" />
<path fill="#ffffff" d="M16.5 28v-6.03L9 17.62z" />
<path fill="#ffffff" fillOpacity="0.2" d="M16.5 20.57l7.5-4.35-7.5-3.35z" />
<path fill="#ffffff" fillOpacity="0.6" d="M9 16.22l7.5 4.35v-7.7z" />
</svg>
)}
{token.symbol === 'stETH' && (
<svg
viewBox="0 0 32 32"
className="size-5 shrink-0 rounded-full shadow-xs"
aria-hidden="true"
>
<circle cx="16" cy="16" r="16" fill="#00A3FF" />
<path fill="#ffffff" fillOpacity="0.8" d="M16 4.5l-6.5 10.8 6.5 3.9 6.5-3.9z" />
<path fill="#ffffff" d="M16 19.2l-6.5-3.9L16 27.5l6.5-12.2z" />
<circle cx="16" cy="16" r="3.5" fill="#ffffff" fillOpacity="0.3" />
</svg>
)}
{token.symbol === 'wstETH' && (
<svg
viewBox="0 0 32 32"
className="size-5 shrink-0 rounded-full shadow-xs"
aria-hidden="true"
>
<circle cx="16" cy="16" r="16" fill="#0052FF" />
<path fill="#ffffff" d="M16 5.5l-5.8 9.5 5.8 3.5 5.8-3.5z" />
<path fill="#ffffff" fillOpacity="0.6" d="M16 18.5l-5.8-3.5 5.8 11 5.8-11z" />
<circle
cx="16"
cy="16"
r="13.5"
stroke="#F5AC37"
strokeWidth="2"
fill="none"
strokeDasharray="3 2"
/>
</svg>
)}
<span className="text-foreground text-xs font-semibold">{token.symbol}</span>
</div>
{isSelected && (
<span className="text-primary size-3.5" aria-hidden="true">
<Check className="size-3.5" />
</span>
)}
</div>
<span className="text-muted-foreground text-xs tabular-nums">{token.balance} avail.</span>
</button>
)
})}
</div>
</div>
{/* Amount Input & Quick Percentages */}
<div className="bg-muted/40 border-border/80 space-y-3 rounded-lg border p-4">
<div className="text-muted-foreground flex items-center justify-between text-xs">
<label htmlFor="stake-amount-input-react" className="text-foreground font-medium">
Deposit Amount
</label>
<div className="flex items-center gap-1.5">
<span className="tabular-nums">
Wallet: {currentToken.balance} {currentToken.symbol}
</span>
<button
type="button"
className="text-primary hover:text-primary/80 inline-flex min-h-6 items-center px-1 font-semibold uppercase transition-colors focus-visible:outline-none"
onClick={setMaxAmount}
>
MAX
</button>
</div>
</div>
{/* Input Row */}
<div className="flex items-center justify-between gap-3">
<Input
id="stake-amount-input-react"
value={stakeAmountStr}
onChange={(e) => setStakeAmountStr(e.target.value)}
type="number"
step="any"
min="0"
className="h-9 border-none bg-transparent px-0 text-xl font-bold tracking-tight tabular-nums shadow-none focus-visible:ring-0"
placeholder="0.0"
/>
<div className="border-border bg-background flex shrink-0 items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-semibold shadow-xs">
<span>{currentToken.symbol}</span>
</div>
</div>
{/* USD Approximation & Presets */}
<div className="flex flex-wrap items-center justify-between gap-2 pt-1 text-xs">
<span className="text-muted-foreground tabular-nums">≈ ${stakedFiatValue} USD</span>
<div className="flex items-center gap-1">
{[25, 50, 75, 100].map((pct) => (
<button
key={pct}
type="button"
className="border-border bg-background hover:bg-muted text-muted-foreground hover:text-foreground min-h-6 rounded border px-2 py-0.5 font-mono text-xs transition-[colors,transform] duration-150 focus-visible:ring-1 focus-visible:outline-none active:scale-95"
onClick={() => setPercentageAmount(pct)}
>
{pct === 100 ? 'MAX' : `${pct}%`}
</button>
))}
</div>
</div>
{/* Interactive Slider */}
<div className="pt-2">
<Slider
value={[Math.min(parsedAmount, currentToken.balance)]}
onValueChange={handleSliderChange}
min={0}
max={currentToken.balance}
step={0.1}
className="w-full"
/>
</div>
</div>
{/* Lockup Duration Selector */}
<div className="space-y-2">
<div className="text-muted-foreground flex items-center justify-between text-xs">
<span className="text-foreground font-medium">Lockup Duration & Boost</span>
<span className="text-muted-foreground">Longer lockups multiply daily yields</span>
</div>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
{lockupOptions.map((opt) => {
const isSelected = selectedLockupId === opt.id
return (
<button
key={opt.id}
type="button"
className={cn(
'border-border/80 focus-visible:ring-ring flex flex-col items-center justify-center gap-1 rounded-lg border p-2.5 text-center transition-[colors,transform] duration-150 focus-visible:ring-2 focus-visible:outline-none active:scale-[0.98]',
isSelected
? 'border-primary bg-primary/5 ring-primary/20 shadow-xs ring-1'
: 'bg-card hover:bg-muted/50 text-muted-foreground',
)}
onClick={() => setSelectedLockupId(opt.id)}
>
<div className="flex items-center gap-1">
{opt.days === 0 ? (
<Unlock className="text-muted-foreground size-3.5" aria-hidden="true" />
) : (
<Lock className="text-primary size-3.5" aria-hidden="true" />
)}
<span className="text-foreground text-xs font-semibold">{opt.label}</span>
</div>
<Badge
variant="outline"
className={cn(
'border-border/60 font-mono text-xs font-normal tabular-nums',
opt.boost > 0 ? 'border-success/30 text-success' : 'text-muted-foreground',
)}
>
{opt.badge}
</Badge>
</button>
)
})}
</div>
</div>
{/* Estimated Returns Highlight Box */}
<div className="border-success/20 bg-success/5 space-y-3 rounded-lg border p-4">
<div className="flex items-center justify-between">
<span className="text-foreground flex items-center gap-1.5 text-xs font-semibold">
<Flame className="text-success size-4" aria-hidden="true" />
Estimated Annual Yield
</span>
<Badge
variant="outline"
className="border-success/30 bg-success/10 text-success font-mono text-xs font-semibold"
>
{effectiveApy}% APY
</Badge>
</div>
{/* Main Yield Value */}
<div className="flex flex-wrap items-baseline gap-2">
<span className="text-success text-success text-xl font-bold tracking-tight tabular-nums sm:text-2xl">
+${annualYieldUsd} / year
</span>
<span className="text-muted-foreground font-mono text-xs tabular-nums">
({annualYieldEth} {currentToken.symbol})
</span>
</div>
<Separator className="bg-success/20" />
{/* Yield Breakdown Grid */}
<div className="grid grid-cols-2 gap-3 text-xs sm:grid-cols-3">
<div className="space-y-0.5">
<div className="text-muted-foreground">Daily Returns</div>
<div className="text-foreground font-medium tabular-nums">+${dailyYieldUsd}</div>
<div className="text-muted-foreground font-mono text-xs tabular-nums">
{dailyYieldEth} {currentToken.symbol}
</div>
</div>
<div className="space-y-0.5">
<div className="text-muted-foreground">Monthly Returns</div>
<div className="text-foreground font-medium tabular-nums">+${monthlyYieldUsd}</div>
<div className="text-muted-foreground font-mono text-xs tabular-nums">
{monthlyYieldEth} {currentToken.symbol}
</div>
</div>
<div className="col-span-2 space-y-0.5 sm:col-span-1">
<div className="text-muted-foreground">Network Gas & Fee</div>
<div className="text-foreground flex items-center gap-1 font-medium tabular-nums">
<Fuel className="text-muted-foreground size-3" aria-hidden="true" />
~$2.85 (0% fee)
</div>
<div className="text-success text-xs">No exit penalty</div>
</div>
</div>
</div>
{/* Primary Stake CTA Button */}
<Button
size="lg"
className="w-full gap-2 font-semibold shadow-xs active:scale-[0.98]"
disabled={parsedAmount <= 0 || isStaking}
onClick={handleExecuteStake}
>
{isStaking ? (
<RefreshCw className="size-4 animate-spin" aria-hidden="true" />
) : stakeSuccess ? (
<Check className="text-success size-4" aria-hidden="true" />
) : (
<Zap className="size-4" aria-hidden="true" />
)}
{isStaking
? 'Submitting Staking Transaction...'
: stakeSuccess
? `Successfully Staked ${parsedAmount} ${currentToken.symbol}!`
: `Stake ${parsedAmount > 0 ? `${parsedAmount} ${currentToken.symbol}` : 'ETH'} Now`}
</Button>
<div className="text-muted-foreground flex items-center justify-center gap-2 text-center text-xs">
<ShieldCheck className="text-success size-3.5" aria-hidden="true" />
<span>Smart contracts verified on Etherscan · 100% non-custodial</span>
</div>
</CardContent>
</Card>
</div>
{/* Right Column: Yield Vaults Directory (5 Cols) */}
<div className="space-y-6 lg:col-span-5">
<Card className="border-border/80 shadow-xs">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-foreground text-base font-semibold">Yield Vaults Directory</CardTitle>
<CardDescription className="text-muted-foreground text-xs">
Active liquidity pools and automated yield strategies.
</CardDescription>
</div>
<Badge variant="secondary" className="text-xs font-normal tabular-nums">
3 Vaults Active
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-3.5">
{yieldVaults.map((vault) => (
<div
key={vault.id}
className={cn(
'border-border/80 relative space-y-3 rounded-lg border p-3.5 transition-colors',
vault.isCurrentVault
? 'bg-primary/5 border-primary/40 ring-primary/20 ring-1'
: 'bg-card hover:bg-muted/30',
)}
>
{/* Vault Header Row */}
<div className="flex items-start justify-between gap-2">
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="text-foreground text-sm font-semibold">{vault.name}</span>
{vault.isCurrentVault && (
<Badge
variant="outline"
className="border-primary/40 bg-primary/10 text-primary px-1.5 py-0 text-xs font-medium"
>
Current
</Badge>
)}
</div>
<p className="text-muted-foreground text-xs">{vault.protocol}</p>
</div>
<div className="text-right">
<div className="text-success text-success text-base font-bold tabular-nums sm:text-lg">
{vault.apy} APY
</div>
<div className="text-muted-foreground font-mono text-xs tabular-nums">{vault.tvl}</div>
</div>
</div>
{/* Vault Meta Details */}
<div className="border-border/60 bg-muted/30 flex flex-wrap items-center justify-between gap-2 rounded-md border p-2 text-xs">
<div className="flex items-center gap-1.5">
<Badge
variant="outline"
className={cn(
'px-1.5 py-0 text-xs font-normal',
vault.riskVariant === 'emerald'
? 'border-success/30 bg-success/10 text-success'
: 'border-warning/30 bg-warning/10 text-warning',
)}
>
{vault.risk}
</Badge>
<span className="text-muted-foreground max-w-[140px] truncate sm:max-w-none">
{vault.strategy}
</span>
</div>
<div className="text-muted-foreground font-medium tabular-nums">{vault.metricValue}</div>
</div>
{/* Vault Actions */}
<div className="flex items-center justify-between pt-1">
<span className="text-muted-foreground font-mono text-xs">{vault.pairSymbol}</span>
<Button
variant={vault.isCurrentVault ? 'default' : 'outline'}
size="sm"
className="h-7 px-3 text-xs shadow-xs"
>
<span>{vault.isCurrentVault ? 'Manage Vault' : 'Deposit Pool'}</span>
<ArrowUpRight className="ml-1 size-3" aria-hidden="true" />
</Button>
</div>
</div>
))}
</CardContent>
<CardFooter className="border-border/60 bg-muted/20 flex flex-col gap-2 rounded-b-xl border-t p-3.5 text-xs">
<div className="text-muted-foreground flex items-center gap-2">
<ShieldAlert className="text-warning size-4 shrink-0" aria-hidden="true" />
<span>Yields adjust dynamically with Ethereum validator queue and DEX trading fee volumes.</span>
</div>
</CardFooter>
</Card>
</div>
</div>
</div>
)
}
export default DefiStakingYieldVault
Raw manifest:https://uipkge.dev/r/react/defi-staking-yield-vault.json