Skip to content

Commit 1e18331

Browse files
committed
feat: add title and image upload
1 parent 04018ab commit 1e18331

File tree

1 file changed

+188
-44
lines changed

1 file changed

+188
-44
lines changed

apps/dashboard/src/app/checkout/components/client/CheckoutLinkForm.client.tsx

Lines changed: 188 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
77
import { Input } from "@/components/ui/input";
88
import { Label } from "@/components/ui/label";
99
import { useThirdwebClient } from "@/constants/thirdweb.client";
10-
import { CreditCardIcon } from "lucide-react";
11-
import { useMemo, useState } from "react";
10+
import { ChevronDownIcon, CreditCardIcon } from "lucide-react";
11+
import { useCallback, useMemo, useState } from "react";
1212
import { toast } from "sonner";
1313
import { type ThirdwebClient, defineChain, getContract } from "thirdweb";
1414
import { getCurrencyMetadata } from "thirdweb/extensions/erc20";
15+
import { resolveScheme, upload } from "thirdweb/storage";
16+
import { FileInput } from "../../../../components/shared/FileInput";
1517
import { resolveEns } from "../../../../lib/ens";
1618

1719
export function CheckoutLinkForm() {
@@ -20,23 +22,121 @@ export function CheckoutLinkForm() {
2022
const [recipientAddress, setRecipientAddress] = useState("");
2123
const [tokenAddressWithChain, setTokenAddressWithChain] = useState("");
2224
const [amount, setAmount] = useState("");
25+
const [title, setTitle] = useState("");
26+
const [image, setImage] = useState<File | null>(null);
27+
const [imageUri, setImageUri] = useState<string>("");
28+
const [uploadingImage, setUploadingImage] = useState(false);
2329
const [isLoading, setIsLoading] = useState(false);
2430
const [error, setError] = useState<string>();
31+
const [showAdvanced, setShowAdvanced] = useState(false);
2532

2633
const isFormComplete = useMemo(() => {
2734
return chainId && recipientAddress && tokenAddressWithChain && amount;
2835
}, [chainId, recipientAddress, tokenAddressWithChain, amount]);
2936

30-
const handleSubmit = async (e: React.FormEvent) => {
31-
e.preventDefault();
32-
setError(undefined);
33-
setIsLoading(true);
37+
const handleImageUpload = useCallback(
38+
async (file: File) => {
39+
try {
40+
setImage(file);
41+
setUploadingImage(true);
3442

35-
try {
36-
if (!chainId || !recipientAddress || !tokenAddressWithChain || !amount) {
37-
throw new Error("All fields are required");
43+
const uri = await upload({
44+
client,
45+
files: [file],
46+
});
47+
48+
// Resolve the IPFS URI for display
49+
const resolvedUrl = resolveScheme({
50+
uri,
51+
client,
52+
});
53+
54+
setImageUri(resolvedUrl);
55+
toast.success("Image uploaded successfully");
56+
} catch (error) {
57+
console.error("Error uploading image:", error);
58+
toast.error("Failed to upload image");
59+
setImage(null);
60+
} finally {
61+
setUploadingImage(false);
3862
}
63+
},
64+
[client],
65+
);
66+
67+
const handleSubmit = useCallback(
68+
async (e: React.FormEvent) => {
69+
e.preventDefault();
70+
setError(undefined);
71+
setIsLoading(true);
72+
73+
try {
74+
if (
75+
!chainId ||
76+
!recipientAddress ||
77+
!tokenAddressWithChain ||
78+
!amount
79+
) {
80+
throw new Error("All fields are required");
81+
}
82+
83+
const inputs = await parseInputs(
84+
client,
85+
chainId,
86+
tokenAddressWithChain,
87+
recipientAddress,
88+
amount,
89+
);
90+
91+
// Build checkout URL
92+
const params = new URLSearchParams({
93+
chainId: inputs.chainId.toString(),
94+
recipientAddress: inputs.recipientAddress,
95+
tokenAddress: inputs.tokenAddress,
96+
amount: inputs.amount.toString(),
97+
});
98+
99+
// Add title as name parameter if provided
100+
if (title) {
101+
params.set("name", title);
102+
}
39103

104+
// Add image URI if available
105+
if (imageUri) {
106+
params.set("image", imageUri);
107+
}
108+
109+
const checkoutUrl = `${window.location.origin}/checkout?${params.toString()}`;
110+
111+
// Copy to clipboard
112+
await navigator.clipboard.writeText(checkoutUrl);
113+
114+
// Show success toast
115+
toast.success("Checkout link copied to clipboard.");
116+
} catch (err) {
117+
setError(err instanceof Error ? err.message : "An error occurred");
118+
} finally {
119+
setIsLoading(false);
120+
}
121+
},
122+
[
123+
amount,
124+
chainId,
125+
client,
126+
imageUri,
127+
recipientAddress,
128+
title,
129+
tokenAddressWithChain,
130+
],
131+
);
132+
133+
const handlePreview = useCallback(async () => {
134+
if (!chainId || !recipientAddress || !tokenAddressWithChain || !amount) {
135+
toast.error("Please fill in all fields first");
136+
return;
137+
}
138+
139+
try {
40140
const inputs = await parseInputs(
41141
client,
42142
chainId,
@@ -45,27 +145,36 @@ export function CheckoutLinkForm() {
45145
amount,
46146
);
47147

48-
// Build checkout URL
49148
const params = new URLSearchParams({
50149
chainId: inputs.chainId.toString(),
51150
recipientAddress: inputs.recipientAddress,
52151
tokenAddress: inputs.tokenAddress,
53152
amount: inputs.amount.toString(),
54153
});
55154

56-
const checkoutUrl = `${window.location.origin}/checkout?${params.toString()}`;
155+
// Add title as name parameter if provided
156+
if (title) {
157+
params.set("name", title);
158+
}
57159

58-
// Copy to clipboard
59-
await navigator.clipboard.writeText(checkoutUrl);
160+
// Add image URI if available
161+
if (imageUri) {
162+
params.set("image", imageUri);
163+
}
60164

61-
// Show success toast
62-
toast.success("Checkout link copied to clipboard.");
165+
window.open(`/checkout?${params.toString()}`, "_blank");
63166
} catch (err) {
64-
setError(err instanceof Error ? err.message : "An error occurred");
65-
} finally {
66-
setIsLoading(false);
167+
toast.error(err instanceof Error ? err.message : "An error occurred");
67168
}
68-
};
169+
}, [
170+
amount,
171+
chainId,
172+
client,
173+
imageUri,
174+
recipientAddress,
175+
title,
176+
tokenAddressWithChain,
177+
]);
69178

70179
return (
71180
<Card className="mx-auto w-full max-w-[500px]">
@@ -138,6 +247,65 @@ export function CheckoutLinkForm() {
138247
/>
139248
</div>
140249

250+
<div className="space-y-4">
251+
<Button
252+
type="button"
253+
variant="ghost"
254+
className="flex w-full items-center justify-between px-0 text-muted-foreground hover:bg-transparent"
255+
onClick={() => setShowAdvanced(!showAdvanced)}
256+
>
257+
<span>Advanced Options</span>
258+
<ChevronDownIcon
259+
className={`size-4 transition-transform duration-200 ease-in-out ${
260+
showAdvanced ? "rotate-180" : ""
261+
}`}
262+
/>
263+
</Button>
264+
265+
<div
266+
className={`grid transition-all duration-200 ease-in-out ${
267+
showAdvanced
268+
? "grid-rows-[1fr] opacity-100"
269+
: "grid-rows-[0fr] opacity-0"
270+
}`}
271+
>
272+
<div className="overflow-hidden">
273+
<div className="space-y-6 pt-2">
274+
<div className="space-y-2">
275+
<Label htmlFor="title" className="font-medium text-sm">
276+
Title
277+
</Label>
278+
<Input
279+
id="title"
280+
value={title}
281+
onChange={(e) => setTitle(e.target.value)}
282+
placeholder="Checkout for..."
283+
className="w-full"
284+
/>
285+
</div>
286+
287+
<div className="space-y-2">
288+
<Label htmlFor="image" className="font-medium text-sm">
289+
Image
290+
</Label>
291+
<div className="w-full pb-1 px-1">
292+
<FileInput
293+
accept={{ "image/*": [] }}
294+
setValue={handleImageUpload}
295+
value={image || imageUri}
296+
className="!rounded-md aspect-square h-24 w-full"
297+
isDisabled={uploadingImage}
298+
selectOrUpload="Upload"
299+
helperText="image"
300+
fileUrl={imageUri}
301+
/>
302+
</div>
303+
</div>
304+
</div>
305+
</div>
306+
</div>
307+
</div>
308+
141309
{error && <div className="text-red-500 text-sm">{error}</div>}
142310

143311
<div className="flex gap-2">
@@ -146,31 +314,7 @@ export function CheckoutLinkForm() {
146314
variant="outline"
147315
className="flex-1"
148316
disabled={isLoading || !isFormComplete}
149-
onClick={async () => {
150-
if (
151-
!chainId ||
152-
!recipientAddress ||
153-
!tokenAddressWithChain ||
154-
!amount
155-
) {
156-
toast.error("Please fill in all fields first");
157-
return;
158-
}
159-
const inputs = await parseInputs(
160-
client,
161-
chainId,
162-
tokenAddressWithChain,
163-
recipientAddress,
164-
amount,
165-
);
166-
const params = new URLSearchParams({
167-
chainId: inputs.chainId.toString(),
168-
recipientAddress: inputs.recipientAddress,
169-
tokenAddress: inputs.tokenAddress,
170-
amount: inputs.amount.toString(),
171-
});
172-
window.open(`/checkout?${params.toString()}`, "_blank");
173-
}}
317+
onClick={handlePreview}
174318
>
175319
Preview
176320
</Button>

0 commit comments

Comments
 (0)