Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
3 changes: 3 additions & 0 deletions advanced/wallets/react-wallet-v2/next.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
module.exports = {
reactStrictMode: true,
images: {
domains: ['cryptologos.cc', 'avatars.githubusercontent.com']
},
webpack(config) {
config.resolve.fallback = {
...config.resolve.fallback,
Expand Down
14 changes: 14 additions & 0 deletions advanced/wallets/react-wallet-v2/public/icons/earn-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
109 changes: 109 additions & 0 deletions advanced/wallets/react-wallet-v2/src/components/Earn/AmountInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Input, Button, Row, Text, styled } from '@nextui-org/react'
import { ChangeEvent, useMemo } from 'react'

const StyledText = styled(Text, {
fontWeight: 400
} as any)
Comment on lines +4 to +6
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

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

Unused variable StyledText.

Suggested change
const StyledText = styled(Text, {
fontWeight: 400
} as any)

Copilot uses AI. Check for mistakes.

interface AmountInputProps {
value: string
onChange: (value: string) => void
balance: string
tokenSymbol: string
disabled?: boolean
label?: string
placeholder?: string
}

export default function AmountInput({
value,
onChange,
balance,
tokenSymbol,
disabled = false,
label = 'Amount to Deposit',
placeholder = '0.00'
}: AmountInputProps) {
const handleChange = (e: ChangeEvent<any>) => {
const inputValue = e.target.value
// Allow only numbers and decimal point
if (inputValue === '' || /^\d*\.?\d*$/.test(inputValue)) {
onChange(inputValue)
}
}

const handleMaxClick = () => {
onChange(balance)
}

const isMaxDisabled = useMemo(() => {
return disabled || !balance || balance === '0'
}, [disabled, balance])

const formattedBalance = useMemo(() => {
if (!balance || balance === '0') return '0.00'
const num = parseFloat(balance)
if (isNaN(num)) return '0.00'
return num.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
}, [balance])

return (
<div style={{ width: '100%' }}>
<Row justify="space-between" align="center" css={{ marginBottom: '$4' }}>
<Text css={{ fontSize: '12px', fontWeight: '600' }}>{label}</Text>
<Text css={{ fontSize: '12px', color: '$gray600' }}>
Balance: {formattedBalance} {tokenSymbol}
</Text>
</Row>

<div style={{ position: 'relative' }}>
<Input
type="text"
value={value}
onChange={handleChange}
placeholder={placeholder}
disabled={disabled}
size="lg"
width="100%"
css={{
'& input': {
fontSize: '16px',
fontWeight: '600',
paddingRight: '120px'
},
'& .nextui-input-wrapper': {
border: '1px solid rgba(255, 255, 255, 0.1)'
}
}}
/>
<div
style={{
position: 'absolute',
right: '12px',
top: '50%',
transform: 'translateY(-50%)',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}
>
<Text css={{ fontSize: '14px', color: '$gray600' }}>{tokenSymbol}</Text>
<Button
size="xs"
auto
flat
color="primary"
onClick={handleMaxClick}
disabled={isMaxDisabled}
css={{ fontSize: '11px', minWidth: '50px' }}
>
MAX
</Button>
</div>
</div>
</div>
)
}
210 changes: 210 additions & 0 deletions advanced/wallets/react-wallet-v2/src/components/Earn/PositionCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { Card, Row, Col, Text, Button, styled, Input, Divider } from '@nextui-org/react'
import { UserPosition } from '@/types/earn'
import { PROTOCOL_CONFIGS } from '@/data/EarnProtocolsData'
import { useMemo, useState, ChangeEvent } from 'react'

// Simple Badge component since NextUI v1 doesn't have Badge
const Badge = styled('span', {
display: 'inline-block',
padding: '2px 8px',
borderRadius: '4px',
fontSize: '16px',
fontWeight: '500',
backgroundColor: 'rgba(34, 197, 94, 0.1)',
color: 'rgb(34, 197, 94)'
} as any)

const StyledCard = styled(Card, {
padding: '0px',
marginBottom: '12px',
border: '1px solid rgba(255, 255, 255, 0.1)'
} as any)

const StyledText = styled(Text, {
fontWeight: 400
} as any)

interface PositionCardProps {
position: UserPosition
onWithdraw: (position: UserPosition, amount: string) => void
}

export default function PositionCard({ position, onWithdraw }: PositionCardProps) {
const [withdrawAmount, setWithdrawAmount] = useState('')

const protocolConfig = useMemo(
() =>
PROTOCOL_CONFIGS.find(
p => p.protocol.id === position.protocol && p.chainId === position.chainId
),
[position.protocol, position.chainId]
)

const protocol = protocolConfig?.protocol

const formattedDepositDate = useMemo(() => {
const date = new Date(position.depositedAt)
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
})
}, [position.depositedAt])

Comment on lines +45 to +53
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

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

Unused variable formattedDepositDate.

Suggested change
const formattedDepositDate = useMemo(() => {
const date = new Date(position.depositedAt)
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
})
}, [position.depositedAt])

Copilot uses AI. Check for mistakes.
const durationDays = useMemo(() => {
const now = Date.now()
const diff = now - position.depositedAt
return Math.floor(diff / (1000 * 60 * 60 * 24))
}, [position.depositedAt])

Comment on lines +54 to +59
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

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

Unused variable durationDays.

Suggested change
const durationDays = useMemo(() => {
const now = Date.now()
const diff = now - position.depositedAt
return Math.floor(diff / (1000 * 60 * 60 * 24))
}, [position.depositedAt])

Copilot uses AI. Check for mistakes.
const handleWithdrawAmountChange = (e: ChangeEvent<any>) => {
const inputValue = e.target.value
// Allow only numbers and decimal point
if (inputValue === '' || /^\d*\.?\d*$/.test(inputValue)) {
setWithdrawAmount(inputValue)
}
}

const handleMaxClick = () => {
setWithdrawAmount(position.total)
}

const handleWithdrawClick = () => {
onWithdraw(position, withdrawAmount)
}

return (
<StyledCard>
{/* Header Row: Protocol Name and APY */}
<Row align="center" justify="space-between" css={{ marginBottom: '2px' }}>
<Text css={{ margin: 0, fontSize: '16px', fontWeight: '600', lineHeight: 1.2 }}>
{protocol?.displayName || position.protocol}
</Text>
<Badge>{position.apy.toFixed(2)}% APY</Badge>
</Row>

{/* Second Row: Token and Chain */}
<Row css={{ marginBottom: '16px' }}>
<StyledText css={{ fontSize: '12px', color: '$gray600' }}>
{position.token} • Base
</StyledText>
</Row>

{/* Third Row: Deposit Amount (left) and Rewards (right) */}
<Row justify="space-between" css={{ marginBottom: '16px' }}>
<Col css={{ width: 'auto' }}>
<StyledText css={{ fontSize: '11px', color: '$gray600', marginBottom: '4px' }}>
Deposit Amount
</StyledText>
<Text weight="semibold" css={{ margin: 0, fontSize: '13px' }}>
{parseFloat(position.principal).toLocaleString('en-US', {
minimumFractionDigits: 0,
maximumFractionDigits: 2
})}{' '}
{position.token}
</Text>
</Col>

<Col css={{ width: 'auto', textAlign: 'right' }}>
<StyledText css={{ fontSize: '11px', color: '$gray600', marginBottom: '4px' }}>
Rewards Earned
</StyledText>
<Text weight="semibold" css={{ margin: 0, fontSize: '13px', color: '$success' }}>
+
{parseFloat(position.rewards).toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 6
})}{' '}
{position.token}
</Text>
</Col>
</Row>
<Divider />
{/* Withdrawal Input */}
<Row css={{ margin: '12px 0' }}>
<div style={{ width: '100%' }}>
<Row justify="space-between" align="center" css={{ marginBottom: '$2' }}>
<Text css={{ fontSize: '11px', fontWeight: '600' }}>Amount to Withdraw</Text>
<Text css={{ fontSize: '11px', color: '$gray600' }}>
Available:{' '}
{parseFloat(position.total).toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 6
})}{' '}
{position.token}
</Text>
</Row>

<div style={{ position: 'relative' }}>
<Input
type="text"
value={withdrawAmount}
onChange={handleWithdrawAmountChange}
placeholder="0.00"
size="lg"
width="100%"
css={{
'& input': {
fontSize: '14px',
fontWeight: '600',
paddingRight: '120px'
},
'& .nextui-input-wrapper': {
border: '1px solid rgba(255, 255, 255, 0.1)'
}
}}
/>
<div
style={{
position: 'absolute',
right: '12px',
top: '50%',
transform: 'translateY(-50%)',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}
>
<Text css={{ fontSize: '13px', color: '$gray600' }}>{position.token}</Text>
<Button
size="xs"
auto
flat
color="primary"
onClick={handleMaxClick}
css={{ fontSize: '11px', minWidth: '50px' }}
>
MAX
</Button>
</div>
</div>
</div>
</Row>
<Row>
{/* Withdraw Button */}
<div style={{ marginTop: '12px', width: '100%' }}>
<Button
onClick={handleWithdrawClick}
disabled={
!withdrawAmount ||
parseFloat(withdrawAmount) <= 0 ||
parseFloat(withdrawAmount) > parseFloat(position.total)
}
css={{
fontSize: '11px',
width: '100%',
height: '36px',
minWidth: 'auto',
backgroundColor: 'rgb(99, 102, 241)',
'&:hover': {
backgroundColor: 'rgb(79, 82, 221)'
}
}}
>
Withdraw
</Button>
</div>
</Row>
</StyledCard>
)
}
Loading