Skip to content

Commit b37c3cc

Browse files
authored
Merge pull request #1206 from peanutprotocol/feat/profile-exchange-rate
[TASK-14416] Feat/profile exchange rate
2 parents 6d0d88d + 9ac02d4 commit b37c3cc

4 files changed

Lines changed: 273 additions & 226 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use client'
2+
3+
import PageContainer from '@/components/0_Bruddle/PageContainer'
4+
import ExchangeRateWidget from '@/components/Global/ExchangeRateWidget'
5+
import NavHeader from '@/components/Global/NavHeader'
6+
import { useRouter } from 'next/navigation'
7+
8+
export default function ExchangeRatePage() {
9+
const router = useRouter()
10+
11+
return (
12+
<PageContainer className="flex flex-col">
13+
<NavHeader title="Exchange rate & fees" onPrev={() => router.replace('/profile')} />
14+
<div className="m-auto">
15+
<ExchangeRateWidget
16+
ctaIcon="arrow-down"
17+
ctaLabel="Add money to try it"
18+
ctaAction={() => router.push('/add-money')}
19+
/>
20+
</div>
21+
</PageContainer>
22+
)
23+
}
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import CurrencySelect from '@/components/LandingPage/CurrencySelect'
2+
import { countryCurrencyMappings } from '@/constants/countryCurrencyMapping'
3+
import { useDebounce } from '@/hooks/useDebounce'
4+
import { useExchangeRate } from '@/hooks/useExchangeRate'
5+
import Image from 'next/image'
6+
import { useRouter, useSearchParams } from 'next/navigation'
7+
import { FC, useCallback, useEffect, useMemo } from 'react'
8+
import { Icon, IconName } from '../Icons/Icon'
9+
import { Button } from '@/components/0_Bruddle'
10+
11+
interface IExchangeRateWidgetProps {
12+
ctaLabel: string
13+
ctaIcon: IconName
14+
ctaAction: () => void
15+
}
16+
17+
const ExchangeRateWidget: FC<IExchangeRateWidgetProps> = ({ ctaLabel, ctaIcon, ctaAction }) => {
18+
const searchParams = useSearchParams()
19+
const router = useRouter()
20+
21+
// Get values from URL or use defaults
22+
const sourceCurrency = searchParams.get('from') || 'USD'
23+
const destinationCurrency = searchParams.get('to') || 'EUR'
24+
const rawAmount = searchParams.get('amount')
25+
const parsedAmount = rawAmount !== null ? Number(rawAmount) : 10
26+
const urlSourceAmount = Number.isFinite(parsedAmount) && parsedAmount > 0 ? parsedAmount : 10
27+
28+
// Exchange rate hook handles all the conversion logic
29+
const {
30+
sourceAmount,
31+
destinationAmount,
32+
exchangeRate,
33+
isLoading,
34+
isError,
35+
handleSourceAmountChange,
36+
handleDestinationAmountChange,
37+
getDestinationDisplayValue,
38+
} = useExchangeRate({
39+
sourceCurrency,
40+
destinationCurrency,
41+
initialSourceAmount: urlSourceAmount,
42+
})
43+
44+
const debouncedSourceAmount = useDebounce(sourceAmount, 500)
45+
46+
// Function to update URL parameters
47+
const updateUrlParams = useCallback(
48+
(params: { from?: string; to?: string; amount?: number }) => {
49+
const newSearchParams = new URLSearchParams(searchParams.toString())
50+
51+
if (params.from) newSearchParams.set('from', params.from)
52+
if (params.to) newSearchParams.set('to', params.to)
53+
if (params.amount !== undefined) newSearchParams.set('amount', params.amount.toString())
54+
55+
router.replace(`?${newSearchParams.toString()}`, { scroll: false })
56+
},
57+
[searchParams, router]
58+
)
59+
60+
// Setter functions that update URL
61+
const setSourceCurrency = useCallback(
62+
(currency: string) => {
63+
updateUrlParams({ from: currency })
64+
},
65+
[updateUrlParams]
66+
)
67+
68+
const setDestinationCurrency = useCallback(
69+
(currency: string) => {
70+
updateUrlParams({ to: currency })
71+
},
72+
[updateUrlParams]
73+
)
74+
75+
// Update URL when source amount changes (only for valid numbers)
76+
useEffect(() => {
77+
if (typeof debouncedSourceAmount === 'number' && debouncedSourceAmount !== urlSourceAmount) {
78+
updateUrlParams({ amount: debouncedSourceAmount })
79+
}
80+
}, [debouncedSourceAmount, urlSourceAmount, updateUrlParams])
81+
82+
const sourceCurrencyFlag = useMemo(
83+
() => countryCurrencyMappings.find((currency) => currency.currencyCode === sourceCurrency)?.flagCode,
84+
[sourceCurrency]
85+
)
86+
87+
const destinationCurrencyFlag = useMemo(
88+
() => countryCurrencyMappings.find((currency) => currency.currencyCode === destinationCurrency)?.flagCode,
89+
[destinationCurrency]
90+
)
91+
92+
// Determine delivery time text based on destination currency
93+
const deliveryTimeText = useMemo(() => {
94+
return destinationCurrency === 'USD'
95+
? 'Should arrive in hours. Estimate.'
96+
: 'Should arrive in minutes. Estimate.'
97+
}, [destinationCurrency])
98+
99+
return (
100+
<div className="btn btn-shadow-primary-4 mx-auto mt-12 flex h-fit w-full flex-col items-center justify-center gap-4 bg-white p-7 md:w-[420px]">
101+
<div className="w-full">
102+
<h2 className="text-left text-sm">You Send</h2>
103+
<div className="btn btn-shadow-primary-4 mt-2 flex w-full items-center justify-center gap-4 bg-white p-4">
104+
{isLoading ? (
105+
<div className="flex w-full items-center">
106+
<div className="h-8 w-40 animate-pulse rounded-full bg-grey-2" />
107+
</div>
108+
) : (
109+
<input
110+
min={0}
111+
placeholder="0"
112+
value={sourceAmount === '' ? '' : sourceAmount}
113+
onChange={(e) => {
114+
const inputValue = e.target.value
115+
if (inputValue === '') {
116+
handleSourceAmountChange('')
117+
} else {
118+
const value = parseFloat(inputValue)
119+
handleSourceAmountChange(isNaN(value) ? '' : value)
120+
}
121+
}}
122+
type="number"
123+
className="w-full bg-transparent outline-none"
124+
/>
125+
)}
126+
<CurrencySelect
127+
selectedCurrency={sourceCurrency}
128+
setSelectedCurrency={setSourceCurrency}
129+
// excludeCurrencies={[destinationCurrency]}
130+
trigger={
131+
<button className="flex w-32 items-center gap-2">
132+
<Image
133+
src={`https://flagcdn.com/w320/${sourceCurrencyFlag}.png`}
134+
alt={`${sourceCurrencyFlag} flag`}
135+
width={160}
136+
height={160}
137+
className="size-4 rounded-full object-cover"
138+
/>
139+
{sourceCurrency} <Icon name="chevron-down" className="text-gray-1" size={10} />
140+
</button>
141+
}
142+
/>
143+
</div>
144+
</div>
145+
146+
<div className="w-full">
147+
<h2 className="text-left text-sm">Recipient Gets</h2>
148+
<div className="btn btn-shadow-primary-4 mt-2 flex w-full items-center justify-center gap-4 bg-white p-4">
149+
{isLoading ? (
150+
<div className="flex w-full items-center">
151+
<div className="h-8 w-40 animate-pulse rounded-full bg-grey-2" />
152+
</div>
153+
) : (
154+
<input
155+
min={0}
156+
placeholder="0"
157+
value={getDestinationDisplayValue()}
158+
onChange={(e) => {
159+
const inputValue = e.target.value
160+
if (inputValue === '') {
161+
handleDestinationAmountChange('', '')
162+
} else {
163+
const value = parseFloat(inputValue)
164+
handleDestinationAmountChange(inputValue, isNaN(value) ? '' : value)
165+
}
166+
}}
167+
type="number"
168+
className="w-full bg-transparent outline-none"
169+
/>
170+
)}
171+
<CurrencySelect
172+
selectedCurrency={destinationCurrency}
173+
setSelectedCurrency={setDestinationCurrency}
174+
trigger={
175+
<button className="flex w-32 items-center gap-2">
176+
<Image
177+
src={`https://flagcdn.com/w320/${destinationCurrencyFlag}.png`}
178+
alt={`${destinationCurrencyFlag} flag`}
179+
width={160}
180+
height={160}
181+
className="size-4 rounded-full object-cover"
182+
/>
183+
{destinationCurrency} <Icon name="chevron-down" className="text-gray-1" size={10} />
184+
</button>
185+
}
186+
/>
187+
</div>
188+
</div>
189+
190+
<div className="rounded-full bg-grey-4 px-2 py-[2px] text-xs font-bold text-gray-1">
191+
{isLoading ? (
192+
<div className="mx-auto h-3 w-28 animate-pulse rounded-full bg-grey-2" />
193+
) : isError ? (
194+
<span>Rate currently unavailable</span>
195+
) : (
196+
<>
197+
1 {sourceCurrency} = {exchangeRate.toFixed(4)} {destinationCurrency}
198+
</>
199+
)}
200+
</div>
201+
202+
{typeof destinationAmount === 'number' && destinationAmount > 0 && (
203+
<div className="flex w-full flex-col gap-3 rounded-sm border-[1.15px] border-black px-4 py-2">
204+
<div className="flex items-center justify-between">
205+
<h2 className="text-left text-sm font-normal">Bank fee</h2>
206+
<h2 className="text-left text-sm font-normal">Free!</h2>
207+
</div>
208+
209+
<div className="flex items-center justify-between">
210+
<h2 className="text-left text-sm font-normal">Peanut fee</h2>
211+
<h2 className="text-left text-sm font-normal">Free!</h2>
212+
</div>
213+
</div>
214+
)}
215+
216+
<Button
217+
onClick={ctaAction}
218+
icon={ctaIcon}
219+
iconSize={13}
220+
shadowSize="4"
221+
className="w-full text-base font-bold"
222+
>
223+
{ctaLabel}
224+
</Button>
225+
226+
{typeof destinationAmount === 'number' && destinationAmount > 0 && (
227+
<div className="flex items-center">
228+
<Icon name="info" className="text-gray-1" size={10} />
229+
<p className="text-xs text-gray-1">{deliveryTimeText}</p>
230+
</div>
231+
)}
232+
</div>
233+
)
234+
}
235+
236+
export default ExchangeRateWidget

0 commit comments

Comments
 (0)