Skip to content

Commit ec88059

Browse files
committed
Fixes some bugs and addresses feedback
1 parent a6214d0 commit ec88059

File tree

9 files changed

+158
-83
lines changed

9 files changed

+158
-83
lines changed

components/DonateCheckout.tsx renamed to components/donate/DonateCheckout.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import Link from "next/link"
44
import { ChangeEvent, FormEvent, useCallback, useState } from "react"
55
import { FormattedMessage } from "react-intl"
66

7-
import LoadingIcon from "../public/icons/loading.svg?inline"
8-
import ArrowLeftIcon from "../public/ui/arrow-left.svg?inline"
7+
import LoadingIcon from "../../public/icons/loading.svg?inline"
8+
import ArrowLeftIcon from "../../public/ui/arrow-left.svg?inline"
99

10-
import { Button } from "./Button"
11-
import { Input } from "./Input"
10+
import { Button } from "../Button"
11+
import { Input } from "../Input"
1212

1313
interface DonateCheckoutProps {
1414
backUrl?: string
@@ -24,11 +24,11 @@ export function DonateCheckout({
2424
const checkout = useCheckout()
2525

2626
const [email, setEmail] = useState("")
27-
const [message, setMessage] = useState<string | null>(null)
27+
const [errorMessage, setErrorMessage] = useState<string | null>(null)
2828
const [isLoading, setIsLoading] = useState(false)
2929

3030
const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
31-
setMessage(null)
31+
setErrorMessage(null)
3232
setEmail(e.target.value)
3333
}, [])
3434

@@ -39,9 +39,9 @@ export function DonateCheckout({
3939

4040
const result = await checkout.updateEmail(email)
4141
if (result.type === "error") {
42-
setMessage(result.error.message)
42+
setErrorMessage(result.error.message)
4343
} else {
44-
setMessage(null)
44+
setErrorMessage(null)
4545
}
4646
}, [checkout, email])
4747

@@ -53,7 +53,7 @@ export function DonateCheckout({
5353

5454
const result = await checkout.updateEmail(email)
5555
if (result.type === "error") {
56-
setMessage(result.error.message)
56+
setErrorMessage(result.error.message)
5757
setIsLoading(false)
5858
return
5959
}
@@ -63,10 +63,11 @@ export function DonateCheckout({
6363
})
6464

6565
if (confirmResult.type === "error") {
66-
setMessage(confirmResult.error.message)
66+
setErrorMessage(confirmResult.error.message)
6767
setIsLoading(false)
68+
} else {
69+
onComplete()
6870
}
69-
onComplete()
7071
},
7172
[checkout, email, onComplete]
7273
)
@@ -142,7 +143,9 @@ export function DonateCheckout({
142143
</div>
143144

144145
<div className="mt-4">
145-
{message && <p className="text-error text-b3 mb-2">{message}</p>}
146+
{errorMessage && (
147+
<p className="text-error text-b3 mb-2">{errorMessage}</p>
148+
)}
146149
<Button
147150
disabled={isLoading}
148151
dark

components/donate/DonateFooter.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { FormattedMessage } from "react-intl"
2+
3+
import LogoPurple from "../../public/logos/logo-purple.svg?inline"
4+
5+
export function DonateFooter() {
6+
return (
7+
<footer className="flex gap-2 items-center mt-3 text-b4 bg-gray-3 px-8 py-2">
8+
<LogoPurple className="size-6" />
9+
<FormattedMessage
10+
id="donate_widget.footer"
11+
defaultMessage="Donations go to Mastodon gGmbH"
12+
/>
13+
</footer>
14+
)
15+
}

components/DonateWidget.tsx renamed to components/donate/DonateWidget.tsx

Lines changed: 79 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import {
88
} from "react-intl"
99
import { 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"
1413
import 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

2121
export 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

donate/DonatePopup.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export function DonatePopup({
7777
<DialogBackdrop className="fixed inset-0 bg-black/30" />
7878
<div
7979
className={classNames(
80-
"fixed inset-0 flex w-screen items-end sm:items-center justify-center z-10",
80+
"fixed inset-0 flex w-screen items-end sm:items-center justify-center z-20",
8181
"transition-transform"
8282
)}
8383
>
@@ -92,14 +92,14 @@ export function DonatePopup({
9292
className={classNames(
9393
"w-full transition-transform",
9494
currentStep === "loading" && "hidden",
95-
currentStep === "choose" && "h-[28rem]",
95+
currentStep === "choose" && "h-[30rem]",
9696
currentStep === "checkout" && "h-[40rem]",
97-
currentStep === "complete" && "h-[28rem]"
97+
currentStep === "complete" && "h-[30rem]"
9898
)}
9999
ref={iframeRef}
100100
></iframe>
101101
{currentStep === "loading" && (
102-
<p className="flex gap-2 items-center justify-center text-gray-2 p-4 h-[28rem]">
102+
<p className="flex gap-2 items-center justify-center text-gray-2 p-4 h-[30rem]">
103103
<LoadingIcon className="motion-safe:animate-spin size-5" />
104104
<FormattedMessage
105105
id="donate_widget.loading"

pages/donate/checkout.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import { useRouter } from "next/navigation"
66
import { useCallback, useEffect, useMemo } from "react"
77
import { z } from "zod"
88

9-
import { DonateCheckout } from "../../components/DonateCheckout"
9+
import { DonateCheckout } from "../../components/donate/DonateCheckout"
1010
import { sendMessage } from "../../donate/utils"
1111
import { CURRENCIES, DONATION_FREQUENCIES } from "../../types/api"
1212

1313
import { themeSchema } from "../../donate/utils"
14+
import { DonateFooter } from "../../components/donate/DonateFooter"
1415

1516
const primaryColor = "#6364ff"
1617
const hoverColor = "#563acc"
@@ -79,11 +80,19 @@ export default function DonateCheckoutPage({
7980
},
8081
}}
8182
>
82-
<DonateCheckout
83-
onComplete={handleDonate}
84-
className={classNames(theme, "bg-white dark:bg-black p-8")}
85-
backUrl={backUrl}
86-
/>
83+
<div
84+
className={classNames(
85+
theme,
86+
"bg-white dark:bg-black min-h-screen flex flex-col"
87+
)}
88+
>
89+
<DonateCheckout
90+
onComplete={handleDonate}
91+
className="p-8 pb-2 grow"
92+
backUrl={backUrl}
93+
/>
94+
<DonateFooter />
95+
</div>
8796
</CheckoutProvider>
8897
)
8998
}

pages/donate/complete.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export default function DonateCompletePage() {
1515
}, [])
1616

1717
return (
18-
<div className="flex flex-col p-8 gap-2">
18+
<div className="flex flex-col p-8 gap-2 justify-between min-h-screen">
1919
<h1 className="sh1">
2020
<FormattedMessage
2121
id="donate_widget.success.header"

0 commit comments

Comments
 (0)