@@ -8,15 +8,15 @@ import {
88} from "react-intl"
99import { Select } from "@headlessui/react"
1010
11- import CheckIcon from "../public/icons/check.svg?inline"
12- import { Button } from "./Button"
13-
11+ import CheckIcon from "../../public/icons/check.svg?inline"
12+ import DropdownArrowIcon from "../../public/icons/dropdown-arrow.svg?inline"
1413import type {
1514 Currency ,
1615 CampaignResponse ,
1716 DonationFrequency ,
18- } from "../types/api"
19- import { Input } from "./Input"
17+ } from "../../types/api"
18+ import { Button } from "../Button"
19+ import { Input } from "../Input"
2020
2121export type OnDonateFn = (
2222 amount : number ,
@@ -90,48 +90,70 @@ export function DonateWidget({
9090 const [ currentAmount , setCurrentAmount ] = useState (
9191 ( ) => defaultAmount ?? amounts [ frequency ] [ currency ] [ 0 ]
9292 )
93+ const [ amountDisplay , setAmountDisplay ] = useState ( ( ) =>
94+ ( ( defaultAmount ?? amounts [ frequency ] [ currency ] [ 0 ] ) / 100 ) . toFixed ( 2 )
95+ )
9396 const [ dirty , setDirty ] = useState ( false )
9497 const [ loadingCheckout , setLoadingCheckout ] = useState ( false )
9598 const [ error , setError ] = useState < string | null > ( null )
9699
97100 const intl = useIntl ( )
98101
102+ const updateAmount = useCallback ( ( amount : number ) => {
103+ const intAmount = Math . round ( amount )
104+ if ( isNaN ( intAmount ) || intAmount < 100 ) {
105+ return
106+ }
107+ setCurrentAmount ( intAmount )
108+ setAmountDisplay ( ( intAmount / 100 ) . toFixed ( 2 ) )
109+ } , [ ] )
110+
99111 const handleChangeFrequency = useCallback (
100112 ( toFrequency : DonationFrequency ) => ( ) => {
101113 setFrequency ( toFrequency )
102- setCurrentAmount ( amounts [ toFrequency ] [ currency ] [ 0 ] )
114+ updateAmount ( amounts [ toFrequency ] [ currency ] [ 0 ] )
103115 setDirty ( false )
104116 setError ( null )
105117 } ,
106- [ amounts , currency ]
118+ [ amounts , currency , updateAmount ]
107119 )
108120 const handleChangeCurrency = useCallback (
109121 ( toCurrency : Currency ) => {
110122 setCurrency ( toCurrency )
111- setCurrentAmount ( amounts [ frequency ] [ toCurrency ] [ 0 ] )
123+ updateAmount ( amounts [ frequency ] [ toCurrency ] [ 0 ] )
112124 setDirty ( false )
113125 setError ( null )
114126 } ,
115- [ amounts , frequency ]
127+ [ amounts , frequency , updateAmount ]
116128 )
117129 const handleChangeAmount : React . ChangeEventHandler < HTMLInputElement > =
118130 useCallback (
119131 ( event ) => {
120- setCurrentAmount ( event . currentTarget . valueAsNumber * 100 )
121132 setDirty ( true )
122- if ( event . currentTarget . valueAsNumber < 1 ) {
123- setError ( intl . formatMessage ( messages . amountError ) )
124- } else {
125- setError ( null )
133+ const { value, valueAsNumber } = event . currentTarget
134+ setAmountDisplay (
135+ value . replaceAll ( / [ ^ 0 - 9 \. ] + / g, "" ) ||
136+ valueAsNumber . toFixed ( 2 ) ||
137+ ( currentAmount / 100 ) . toFixed ( 2 )
138+ )
139+ if ( isNaN ( valueAsNumber ) || valueAsNumber < 1 ) {
140+ return
126141 }
142+ setCurrentAmount ( valueAsNumber * 100 )
127143 } ,
128- [ intl ]
144+ [ currentAmount ]
129145 )
130- const handleClickAmount = useCallback ( ( amount : number ) => {
131- setCurrentAmount ( amount )
132- setDirty ( false )
133- setError ( null )
134- } , [ ] )
146+ const handleClickAmount = useCallback (
147+ ( amount : number ) => {
148+ updateAmount ( amount )
149+ setDirty ( false )
150+ setError ( null )
151+ } ,
152+ [ updateAmount ]
153+ )
154+ const handleBlurAmount = useCallback ( ( ) => {
155+ setAmountDisplay ( ( currentAmount / 100 ) . toFixed ( 2 ) )
156+ } , [ currentAmount ] )
135157
136158 const handleDonate = useCallback ( ( ) => {
137159 setLoadingCheckout ( true )
@@ -150,52 +172,59 @@ export function DonateWidget({
150172 { frequencies . map ( ( freq ) => (
151173 < Button
152174 key = { freq }
153- className = { classNames (
154- "rounded-none first:rounded-l-md last:rounded-r-md"
155- ) }
175+ className = "rounded-none first:rounded-l-md last:rounded-r-md pr-6 group"
156176 dark = { freq === frequency }
157177 onClick = { handleChangeFrequency ( freq ) }
158178 disabled = { loadingCheckout }
159179 fullWidth
160180 >
161- < CheckIcon className = "fill-black w-auto h-4" />
181+ < CheckIcon
182+ className = { classNames (
183+ "fill-black w-auto h-4 transition-opacity" ,
184+ frequency !== freq && "opacity-0 group-hover:opacity-100"
185+ ) }
186+ />
162187 { intl . formatMessage ( messages [ freq ] ) }
163188 </ Button >
164189 ) ) }
165190 </ div >
166191
167192 < div className = "flex focus-within:shadow-input rounded-md" >
168- < Select
169- className = { classNames (
170- "p-2 rounded-l-md outline-none transition-colors cursor-pointer disabled:cursor-default font-medium" ,
171- "text-white bg-blurple-500 hocus:bg-blurple-600" ,
172- "disabled:bg-gray-2 disabled:hocus:bg-gray-2"
173- ) }
174- value = { currency }
175- onChange = { ( e ) => handleChangeCurrency ( e . target . value as Currency ) }
176- aria-label = { intl . formatMessage ( messages . currencySelect ) }
177- disabled = { loadingCheckout }
178- >
179- < option value = "USD" >
180- < FormattedMessage
181- id = "donate_widget.currency.usd"
182- defaultMessage = "USD"
183- />
184- </ option >
185- < option value = "EUR" >
186- < FormattedMessage
187- id = "donate_widget.currency.eur"
188- defaultMessage = "EUR"
189- />
190- </ option >
191- </ Select >
193+ < span className = "relative" >
194+ < DropdownArrowIcon className = "absolute left-0 top-[9px] fill-white pointer-events-none" />
195+ < Select
196+ className = { classNames (
197+ "h-full p-2 pl-6 rounded-l-md outline-none transition-colors cursor-pointer disabled:cursor-default font-medium" ,
198+ "text-white bg-blurple-500 hocus:bg-blurple-600" ,
199+ "disabled:bg-gray-2 disabled:hocus:bg-gray-2"
200+ ) }
201+ value = { currency }
202+ onChange = { ( e ) => handleChangeCurrency ( e . target . value as Currency ) }
203+ aria-label = { intl . formatMessage ( messages . currencySelect ) }
204+ disabled = { loadingCheckout }
205+ >
206+ < option value = "USD" >
207+ < FormattedMessage
208+ id = "donate_widget.currency.usd"
209+ defaultMessage = "USD"
210+ />
211+ </ option >
212+ < option value = "EUR" >
213+ < FormattedMessage
214+ id = "donate_widget.currency.eur"
215+ defaultMessage = "EUR"
216+ />
217+ </ option >
218+ </ Select >
219+ </ span >
192220 < Input
193221 className = "rounded-l-none focus:shadow-none"
194222 type = "number"
195- value = { currentAmount / 100 }
223+ value = { amountDisplay }
196224 onChange = { handleChangeAmount }
225+ onBlur = { handleBlurAmount }
197226 min = { 1 }
198- step = { 1 }
227+ step = { 0.01 }
199228 aria-label = { intl . formatMessage ( messages . amountSelect ) }
200229 disabled = { loadingCheckout }
201230 fullWidth
0 commit comments