@trustless-work/blocks 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +96 -0
  2. package/bin/index.js +1123 -0
  3. package/package.json +44 -0
  4. package/templates/deps.json +29 -0
  5. package/templates/escrows/details/Actions.tsx +149 -0
  6. package/templates/escrows/details/Entities.tsx +48 -0
  7. package/templates/escrows/details/EntityCard.tsx +98 -0
  8. package/templates/escrows/details/EscrowDetailDialog.tsx +154 -0
  9. package/templates/escrows/details/GeneralInformation.tsx +329 -0
  10. package/templates/escrows/details/MilestoneCard.tsx +254 -0
  11. package/templates/escrows/details/MilestoneDetailDialog.tsx +276 -0
  12. package/templates/escrows/details/Milestones.tsx +87 -0
  13. package/templates/escrows/details/ProgressEscrow.tsx +191 -0
  14. package/templates/escrows/details/StatisticsCard.tsx +79 -0
  15. package/templates/escrows/details/SuccessReleaseDialog.tsx +101 -0
  16. package/templates/escrows/details/useDetailsEscrow.ts +126 -0
  17. package/templates/escrows/escrow-context/EscrowAmountProvider.tsx +86 -0
  18. package/templates/escrows/escrow-context/EscrowDialogsProvider.tsx +108 -0
  19. package/templates/escrows/escrow-context/EscrowProvider.tsx +124 -0
  20. package/templates/escrows/escrows-by-role/cards/EscrowsCards.tsx +503 -0
  21. package/templates/escrows/escrows-by-role/cards/Filters.tsx +421 -0
  22. package/templates/escrows/escrows-by-role/table/EscrowsTable.tsx +427 -0
  23. package/templates/escrows/escrows-by-role/table/Filters.tsx +421 -0
  24. package/templates/escrows/escrows-by-role/useEscrowsByRole.shared.ts +336 -0
  25. package/templates/escrows/escrows-by-signer/cards/EscrowsCards.tsx +502 -0
  26. package/templates/escrows/escrows-by-signer/cards/Filters.tsx +389 -0
  27. package/templates/escrows/escrows-by-signer/table/EscrowsTable.tsx +422 -0
  28. package/templates/escrows/escrows-by-signer/table/Filters.tsx +389 -0
  29. package/templates/escrows/escrows-by-signer/useEscrowsBySigner.shared.ts +320 -0
  30. package/templates/escrows/single-release/approve-milestone/button/ApproveMilestone.tsx +78 -0
  31. package/templates/escrows/single-release/approve-milestone/dialog/ApproveMilestone.tsx +102 -0
  32. package/templates/escrows/single-release/approve-milestone/form/ApproveMilestone.tsx +80 -0
  33. package/templates/escrows/single-release/approve-milestone/shared/schema.ts +9 -0
  34. package/templates/escrows/single-release/approve-milestone/shared/useApproveMilestone.ts +67 -0
  35. package/templates/escrows/single-release/change-milestone-status/button/ChangeMilestoneStatus.tsx +78 -0
  36. package/templates/escrows/single-release/change-milestone-status/dialog/ChangeMilestoneStatus.tsx +167 -0
  37. package/templates/escrows/single-release/change-milestone-status/form/ChangeMilestoneStatus.tsx +114 -0
  38. package/templates/escrows/single-release/change-milestone-status/shared/schema.ts +15 -0
  39. package/templates/escrows/single-release/change-milestone-status/shared/useChangeMilestoneStatus.ts +77 -0
  40. package/templates/escrows/single-release/dispute-escrow/button/DisputeEscrow.tsx +68 -0
  41. package/templates/escrows/single-release/fund-escrow/button/FundEscrow.tsx +84 -0
  42. package/templates/escrows/single-release/fund-escrow/dialog/FundEscrow.tsx +77 -0
  43. package/templates/escrows/single-release/fund-escrow/form/FundEscrow.tsx +54 -0
  44. package/templates/escrows/single-release/fund-escrow/shared/schema.ts +10 -0
  45. package/templates/escrows/single-release/fund-escrow/shared/useFundEscrow.ts +66 -0
  46. package/templates/escrows/single-release/initialize-escrow/dialog/InitializeEscrow.tsx +526 -0
  47. package/templates/escrows/single-release/initialize-escrow/form/InitializeEscrow.tsx +504 -0
  48. package/templates/escrows/single-release/initialize-escrow/shared/schema.ts +232 -0
  49. package/templates/escrows/single-release/initialize-escrow/shared/useInitializeEscrow.ts +115 -0
  50. package/templates/escrows/single-release/release-escrow/button/ReleaseEscrow.tsx +80 -0
  51. package/templates/escrows/single-release/resolve-dispute/button/ResolveDispute.tsx +94 -0
  52. package/templates/escrows/single-release/resolve-dispute/dialog/ResolveDispute.tsx +123 -0
  53. package/templates/escrows/single-release/resolve-dispute/form/ResolveDispute.tsx +82 -0
  54. package/templates/escrows/single-release/resolve-dispute/shared/schema.ts +82 -0
  55. package/templates/escrows/single-release/resolve-dispute/shared/useResolveDispute.ts +58 -0
  56. package/templates/escrows/single-release/update-escrow/dialog/UpdateEscrow.tsx +485 -0
  57. package/templates/escrows/single-release/update-escrow/form/UpdateEscrow.tsx +463 -0
  58. package/templates/escrows/single-release/update-escrow/shared/schema.ts +139 -0
  59. package/templates/escrows/single-release/update-escrow/shared/useUpdateEscrow.ts +211 -0
  60. package/templates/handle-errors/errors.enum.ts +6 -0
  61. package/templates/handle-errors/handle.ts +47 -0
  62. package/templates/helpers/format.helper.ts +27 -0
  63. package/templates/helpers/useCopy.ts +13 -0
  64. package/templates/providers/ReactQueryClientProvider.tsx +28 -0
  65. package/templates/providers/TrustlessWork.tsx +30 -0
  66. package/templates/tanstak/useEscrowsByRoleQuery.ts +87 -0
  67. package/templates/tanstak/useEscrowsBySignerQuery.ts +78 -0
  68. package/templates/tanstak/useEscrowsMutations.ts +411 -0
  69. package/templates/wallet-kit/WalletButtons.tsx +116 -0
  70. package/templates/wallet-kit/WalletProvider.tsx +94 -0
  71. package/templates/wallet-kit/trustlines.ts +40 -0
  72. package/templates/wallet-kit/useWallet.ts +77 -0
  73. package/templates/wallet-kit/validators.ts +12 -0
  74. package/templates/wallet-kit/wallet-kit.ts +30 -0
@@ -0,0 +1,463 @@
1
+ import * as React from "react";
2
+ import {
3
+ Form,
4
+ FormField,
5
+ FormItem,
6
+ FormLabel,
7
+ FormControl,
8
+ FormMessage,
9
+ } from "__UI_BASE__/form";
10
+ import { Input } from "__UI_BASE__/input";
11
+ import { Button } from "__UI_BASE__/button";
12
+ import { Card } from "__UI_BASE__/card";
13
+ import {
14
+ Select,
15
+ SelectTrigger,
16
+ SelectValue,
17
+ SelectContent,
18
+ SelectItem,
19
+ } from "__UI_BASE__/select";
20
+ import { Textarea } from "__UI_BASE__/textarea";
21
+ import { useUpdateEscrow } from "./useUpdateEscrow";
22
+ import { Trash2, DollarSign, Percent, Loader2 } from "lucide-react";
23
+ import Link from "next/link";
24
+ import { trustlineOptions } from "@/components/tw-blocks/wallet-kit/trustlines";
25
+
26
+ export default function UpdateEscrowForm() {
27
+ const {
28
+ form,
29
+ isSubmitting,
30
+ milestones,
31
+ isAnyMilestoneEmpty,
32
+ handleSubmit,
33
+ handleAddMilestone,
34
+ handleRemoveMilestone,
35
+ handleAmountChange,
36
+ handlePlatformFeeChange,
37
+ } = useUpdateEscrow();
38
+
39
+ return (
40
+ <Form {...form}>
41
+ <form onSubmit={handleSubmit} className="flex flex-col space-y-6">
42
+ <Card className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 p-4">
43
+ <Link
44
+ className="flex-1"
45
+ href="https://docs.trustlesswork.com/trustless-work/technology-overview/escrow-types"
46
+ target="_blank"
47
+ >
48
+ <div className="flex items-center gap-2">
49
+ <div className="h-2 w-2 rounded-full bg-primary" />
50
+ <h2 className="text-xl font-semibold">Single Release Escrow</h2>
51
+ </div>
52
+ <p className="text-muted-foreground mt-1">
53
+ Update escrow details and milestones
54
+ </p>
55
+ </Link>
56
+ </Card>
57
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
58
+ <FormField
59
+ control={form.control}
60
+ name="title"
61
+ render={({ field }) => (
62
+ <FormItem>
63
+ <FormLabel className="flex items-center">
64
+ Title<span className="text-destructive ml-1">*</span>
65
+ </FormLabel>
66
+ <FormControl>
67
+ <Input
68
+ placeholder="Escrow title"
69
+ {...field}
70
+ onChange={(e) => {
71
+ field.onChange(e);
72
+ }}
73
+ />
74
+ </FormControl>
75
+ <FormMessage />
76
+ </FormItem>
77
+ )}
78
+ />
79
+
80
+ <FormField
81
+ control={form.control}
82
+ name="engagementId"
83
+ render={({ field }) => (
84
+ <FormItem>
85
+ <FormLabel className="flex items-center">
86
+ Engagement<span className="text-destructive ml-1">*</span>
87
+ </FormLabel>
88
+ <FormControl>
89
+ <Input
90
+ placeholder="Enter identifier"
91
+ {...field}
92
+ onChange={(e) => {
93
+ field.onChange(e);
94
+ }}
95
+ />
96
+ </FormControl>
97
+ <FormMessage />
98
+ </FormItem>
99
+ )}
100
+ />
101
+
102
+ <FormField
103
+ control={form.control}
104
+ name="trustline.address"
105
+ render={({ field }) => (
106
+ <FormItem>
107
+ <FormLabel className="flex items-center">
108
+ Trustline<span className="text-destructive ml-1">*</span>
109
+ </FormLabel>
110
+ <FormControl>
111
+ <Select
112
+ value={field.value}
113
+ onValueChange={(e) => {
114
+ field.onChange(e);
115
+ }}
116
+ >
117
+ <SelectTrigger className="w-full">
118
+ <SelectValue placeholder="Select trustline" />
119
+ </SelectTrigger>
120
+ <SelectContent>
121
+ {trustlineOptions
122
+ .filter((option) => option.value)
123
+ .map((option, index) => (
124
+ <SelectItem
125
+ key={`${option.value}-${index}`}
126
+ value={option.value}
127
+ >
128
+ {option.label}
129
+ </SelectItem>
130
+ ))}
131
+ </SelectContent>
132
+ </Select>
133
+ </FormControl>
134
+ <FormMessage />
135
+ </FormItem>
136
+ )}
137
+ />
138
+ </div>
139
+
140
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
141
+ <FormField
142
+ control={form.control}
143
+ name="roles.approver"
144
+ render={({ field }) => (
145
+ <FormItem>
146
+ <FormLabel className="flex items-center justify-between">
147
+ <span className="flex items-center">
148
+ Approver<span className="text-destructive ml-1">*</span>
149
+ </span>
150
+ </FormLabel>
151
+
152
+ <FormControl>
153
+ <Input
154
+ placeholder="Enter approver address"
155
+ {...field}
156
+ onChange={(e) => {
157
+ field.onChange(e);
158
+ }}
159
+ />
160
+ </FormControl>
161
+ <FormMessage />
162
+ </FormItem>
163
+ )}
164
+ />
165
+
166
+ <FormField
167
+ control={form.control}
168
+ name="roles.serviceProvider"
169
+ render={({ field }) => (
170
+ <FormItem>
171
+ <FormLabel className="flex items-center justify-between">
172
+ <span className="flex items-center">
173
+ Service Provider
174
+ <span className="text-destructive ml-1">*</span>
175
+ </span>
176
+ </FormLabel>
177
+
178
+ <FormControl>
179
+ <Input
180
+ placeholder="Enter service provider address"
181
+ {...field}
182
+ onChange={(e) => {
183
+ field.onChange(e);
184
+ }}
185
+ />
186
+ </FormControl>
187
+ <FormMessage />
188
+ </FormItem>
189
+ )}
190
+ />
191
+ </div>
192
+
193
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
194
+ <FormField
195
+ control={form.control}
196
+ name="roles.releaseSigner"
197
+ render={({ field }) => (
198
+ <FormItem>
199
+ <FormLabel className="flex items-center justify-between">
200
+ <span className="flex items-center">
201
+ Release Signer
202
+ <span className="text-destructive ml-1">*</span>
203
+ </span>
204
+ </FormLabel>
205
+
206
+ <FormControl>
207
+ <Input
208
+ placeholder="Enter release signer address"
209
+ {...field}
210
+ onChange={(e) => {
211
+ field.onChange(e);
212
+ }}
213
+ />
214
+ </FormControl>
215
+ <FormMessage />
216
+ </FormItem>
217
+ )}
218
+ />
219
+
220
+ <FormField
221
+ control={form.control}
222
+ name="roles.disputeResolver"
223
+ render={({ field }) => (
224
+ <FormItem>
225
+ <FormLabel className="flex items-center justify-between">
226
+ <span className="flex items-center">
227
+ Dispute Resolver
228
+ <span className="text-destructive ml-1">*</span>
229
+ </span>
230
+ </FormLabel>
231
+
232
+ <FormControl>
233
+ <Input
234
+ placeholder="Enter dispute resolver address"
235
+ {...field}
236
+ onChange={(e) => {
237
+ field.onChange(e);
238
+ }}
239
+ />
240
+ </FormControl>
241
+ <FormMessage />
242
+ </FormItem>
243
+ )}
244
+ />
245
+ </div>
246
+
247
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
248
+ <FormField
249
+ control={form.control}
250
+ name="roles.platformAddress"
251
+ render={({ field }) => (
252
+ <FormItem>
253
+ <FormLabel className="flex items-center justify-between">
254
+ <span className="flex items-center">
255
+ Platform Address
256
+ <span className="text-destructive ml-1">*</span>
257
+ </span>
258
+ </FormLabel>
259
+
260
+ <FormControl>
261
+ <Input
262
+ placeholder="Enter platform address"
263
+ {...field}
264
+ onChange={(e) => {
265
+ field.onChange(e);
266
+ }}
267
+ />
268
+ </FormControl>
269
+ <FormMessage />
270
+ </FormItem>
271
+ )}
272
+ />
273
+ <FormField
274
+ control={form.control}
275
+ name="roles.receiver"
276
+ render={({ field }) => (
277
+ <FormItem>
278
+ <FormLabel className="flex items-center justify-between">
279
+ <span className="flex items-center">
280
+ Receiver<span className="text-destructive ml-1">*</span>
281
+ </span>
282
+ </FormLabel>
283
+
284
+ <FormControl>
285
+ <Input
286
+ placeholder="Enter receiver address"
287
+ {...field}
288
+ onChange={(e) => {
289
+ field.onChange(e);
290
+ }}
291
+ />
292
+ </FormControl>
293
+ <FormMessage />
294
+ </FormItem>
295
+ )}
296
+ />
297
+ </div>
298
+
299
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
300
+ <FormField
301
+ control={form.control}
302
+ name="platformFee"
303
+ render={() => (
304
+ <FormItem>
305
+ <FormLabel className="flex items-center">
306
+ Platform Fee<span className="text-destructive ml-1">*</span>
307
+ </FormLabel>
308
+ <FormControl>
309
+ <div className="relative">
310
+ <Percent
311
+ className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"
312
+ size={18}
313
+ />
314
+ <Input
315
+ placeholder="Enter platform fee"
316
+ className="pl-10"
317
+ value={form.watch("platformFee")?.toString() || ""}
318
+ onChange={handlePlatformFeeChange}
319
+ />
320
+ </div>
321
+ </FormControl>
322
+ <FormMessage />
323
+ </FormItem>
324
+ )}
325
+ />
326
+
327
+ <FormField
328
+ control={form.control}
329
+ name="amount"
330
+ render={() => (
331
+ <FormItem>
332
+ <FormLabel className="flex items-center">
333
+ Amount<span className="text-destructive ml-1">*</span>
334
+ </FormLabel>
335
+ <FormControl>
336
+ <div className="relative">
337
+ <DollarSign
338
+ className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"
339
+ size={18}
340
+ />
341
+ <Input
342
+ placeholder="Enter amount"
343
+ className="pl-10"
344
+ value={form.watch("amount")?.toString() || ""}
345
+ onChange={handleAmountChange}
346
+ />
347
+ </div>
348
+ </FormControl>
349
+ <FormMessage />
350
+ </FormItem>
351
+ )}
352
+ />
353
+
354
+ <FormField
355
+ control={form.control}
356
+ name="receiverMemo"
357
+ render={({ field }) => (
358
+ <FormItem>
359
+ <FormLabel className="flex items-center">
360
+ Receiver Memo (opcional)
361
+ </FormLabel>
362
+ <FormControl>
363
+ <Input
364
+ type="text"
365
+ placeholder="Enter the escrow receiver Memo"
366
+ {...field}
367
+ onChange={(e) => {
368
+ field.onChange(e);
369
+ }}
370
+ />
371
+ </FormControl>
372
+ <FormMessage />
373
+ </FormItem>
374
+ )}
375
+ />
376
+ </div>
377
+
378
+ <FormField
379
+ control={form.control}
380
+ name="description"
381
+ render={({ field }) => (
382
+ <FormItem>
383
+ <FormLabel className="flex items-center">
384
+ Description<span className="text-destructive ml-1">*</span>
385
+ </FormLabel>
386
+ <FormControl>
387
+ <Textarea
388
+ placeholder="Escrow description"
389
+ {...field}
390
+ onChange={(e) => {
391
+ field.onChange(e);
392
+ }}
393
+ />
394
+ </FormControl>
395
+ <FormMessage />
396
+ </FormItem>
397
+ )}
398
+ />
399
+
400
+ <div className="space-y-4">
401
+ <FormLabel className="flex items-center">
402
+ Milestones<span className="text-destructive ml-1">*</span>
403
+ </FormLabel>
404
+ {milestones.map((milestone, index) => (
405
+ <div key={index}>
406
+ <div className="flex flex-col sm:flex-row items-start sm:items-center space-y-2 sm:space-y-0 sm:space-x-4">
407
+ <Input
408
+ placeholder="Milestone Description"
409
+ value={milestone.description}
410
+ className="w-full sm:flex-1"
411
+ onChange={(e) => {
412
+ const updatedMilestones = [...milestones];
413
+ updatedMilestones[index].description = e.target.value;
414
+ form.setValue("milestones", updatedMilestones);
415
+ }}
416
+ />
417
+
418
+ <Button
419
+ onClick={() => handleRemoveMilestone(index)}
420
+ className="p-2 bg-transparent text-destructive rounded-md border-none shadow-none hover:bg-transparent hover:shadow-none hover:text-destructive focus:ring-0 active:ring-0 self-start sm:self-center cursor-pointer"
421
+ disabled={milestones.length === 1}
422
+ >
423
+ <Trash2 className="h-5 w-5" />
424
+ </Button>
425
+ </div>
426
+
427
+ {index === milestones.length - 1 && (
428
+ <div className="flex justify-end mt-4">
429
+ <Button
430
+ disabled={isAnyMilestoneEmpty}
431
+ className="w-full md:w-1/4 cursor-pointer"
432
+ variant="outline"
433
+ onClick={handleAddMilestone}
434
+ type="button"
435
+ >
436
+ Add Item
437
+ </Button>
438
+ </div>
439
+ )}
440
+ </div>
441
+ ))}
442
+ </div>
443
+
444
+ <div className="flex justify-start">
445
+ <Button
446
+ className="w-full md:w-1/4 cursor-pointer"
447
+ type="submit"
448
+ disabled={isAnyMilestoneEmpty || isSubmitting}
449
+ >
450
+ {isSubmitting ? (
451
+ <div className="flex items-center">
452
+ <Loader2 className="h-5 w-5 animate-spin" />
453
+ <span className="ml-2">Updating...</span>
454
+ </div>
455
+ ) : (
456
+ "Update"
457
+ )}
458
+ </Button>
459
+ </div>
460
+ </form>
461
+ </Form>
462
+ );
463
+ }
@@ -0,0 +1,139 @@
1
+ import { z } from "zod";
2
+ import { isValidWallet } from "@/components/tw-blocks/wallet-kit/validators";
3
+
4
+ export const useUpdateEscrowSchema = () => {
5
+ const getBaseSchema = () => {
6
+ return z.object({
7
+ trustline: z.object({
8
+ address: z.string().min(1, {
9
+ message: "Trustline address is required.",
10
+ }),
11
+ decimals: z.number().default(10000000),
12
+ }),
13
+ roles: z.object({
14
+ approver: z
15
+ .string()
16
+ .min(1, { message: "Approver is required." })
17
+ .refine((value) => isValidWallet(value), {
18
+ message: "Approver must be a valid wallet.",
19
+ }),
20
+ serviceProvider: z
21
+ .string()
22
+ .min(1, { message: "Service provider is required." })
23
+ .refine((value) => isValidWallet(value), {
24
+ message: "Service provider must be a valid wallet.",
25
+ }),
26
+ platformAddress: z
27
+ .string()
28
+ .min(1, { message: "Platform address is required." })
29
+ .refine((value) => isValidWallet(value), {
30
+ message: "Platform address must be a valid wallet.",
31
+ }),
32
+ releaseSigner: z
33
+ .string()
34
+ .min(1, { message: "Release signer is required." })
35
+ .refine((value) => isValidWallet(value), {
36
+ message: "Release signer must be a valid wallet.",
37
+ }),
38
+ disputeResolver: z
39
+ .string()
40
+ .min(1, { message: "Dispute resolver is required." })
41
+ .refine((value) => isValidWallet(value), {
42
+ message: "Dispute resolver must be a valid wallet.",
43
+ }),
44
+ receiver: z
45
+ .string()
46
+ .min(1, { message: "Receiver address is required." })
47
+ .refine((value) => isValidWallet(value), {
48
+ message: "Receiver address must be a valid wallet.",
49
+ }),
50
+ }),
51
+ engagementId: z.string().min(1, { message: "Engagement is required." }),
52
+ title: z.string().min(1, { message: "Title is required." }),
53
+ description: z.string().min(10, {
54
+ message: "Description must be at least 10 characters long.",
55
+ }),
56
+ platformFee: z
57
+ .union([z.string(), z.number()])
58
+ .refine(
59
+ (val) => {
60
+ if (typeof val === "string") {
61
+ if (val === "" || val === "." || val.endsWith(".")) return true;
62
+ const n = Number(val);
63
+ return !isNaN(n) && n > 0;
64
+ }
65
+ return val > 0;
66
+ },
67
+ { message: "Platform fee must be greater than 0." }
68
+ )
69
+ .refine(
70
+ (val) => {
71
+ if (typeof val === "string") {
72
+ if (val === "" || val === "." || val.endsWith(".")) return true;
73
+ const n = Number(val);
74
+ if (isNaN(n)) return false;
75
+ const dp = (n.toString().split(".")[1] || "").length;
76
+ return dp <= 2;
77
+ }
78
+ const dp = (val.toString().split(".")[1] || "").length;
79
+ return dp <= 2;
80
+ },
81
+ { message: "Platform fee can have a maximum of 2 decimal places." }
82
+ ),
83
+ receiverMemo: z
84
+ .string()
85
+ .optional()
86
+ .refine((val) => !val || val.length >= 1, {
87
+ message: "Receiver Memo must be at least 1.",
88
+ })
89
+ .refine((val) => !val || /^[1-9][0-9]*$/.test(val), {
90
+ message:
91
+ "Receiver Memo must be a whole number greater than 0 (no decimals).",
92
+ }),
93
+ });
94
+ };
95
+
96
+ const getSingleReleaseFormSchema = () => {
97
+ const baseSchema = getBaseSchema();
98
+ return baseSchema.extend({
99
+ amount: z
100
+ .union([z.string(), z.number()])
101
+ .refine(
102
+ (val) => {
103
+ if (typeof val === "string") {
104
+ if (val === "" || val === "." || val.endsWith(".")) return true;
105
+ const n = Number(val);
106
+ return !isNaN(n) && n > 0;
107
+ }
108
+ return val > 0;
109
+ },
110
+ { message: "Amount must be greater than 0." }
111
+ )
112
+ .refine(
113
+ (val) => {
114
+ if (typeof val === "string") {
115
+ if (val === "" || val === "." || val.endsWith(".")) return true;
116
+ const n = Number(val);
117
+ if (isNaN(n)) return false;
118
+ const dp = (n.toString().split(".")[1] || "").length;
119
+ return dp <= 2;
120
+ }
121
+ const dp = (val.toString().split(".")[1] || "").length;
122
+ return dp <= 2;
123
+ },
124
+ { message: "Amount can have a maximum of 2 decimal places." }
125
+ ),
126
+ milestones: z
127
+ .array(
128
+ z.object({
129
+ description: z
130
+ .string()
131
+ .min(1, { message: "Milestone description is required." }),
132
+ })
133
+ )
134
+ .min(1, { message: "At least one milestone is required." }),
135
+ });
136
+ };
137
+
138
+ return { getSingleReleaseFormSchema };
139
+ };