@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,276 @@
1
+ "use client";
2
+
3
+ import { Button } from "__UI_BASE__/button";
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ } from "__UI_BASE__/dialog";
10
+ import { Card, CardContent, CardHeader, CardTitle } from "__UI_BASE__/card";
11
+ import { Badge } from "__UI_BASE__/badge";
12
+ import {
13
+ DollarSign,
14
+ FileCheck2,
15
+ User,
16
+ Calendar,
17
+ Hash,
18
+ ExternalLink,
19
+ CircleAlert,
20
+ CircleCheckBig,
21
+ Handshake,
22
+ CheckCheck,
23
+ Layers,
24
+ } from "lucide-react";
25
+ import {
26
+ GetEscrowsFromIndexerResponse,
27
+ MultiReleaseMilestone,
28
+ Role,
29
+ SingleReleaseMilestone,
30
+ } from "@trustless-work/escrow";
31
+ import Link from "next/link";
32
+
33
+ interface MilestoneDetailDialogProps {
34
+ isOpen: boolean;
35
+ onClose: () => void;
36
+ selectedMilestone: {
37
+ milestone: MultiReleaseMilestone | SingleReleaseMilestone;
38
+ index: number;
39
+ } | null;
40
+ selectedEscrow: GetEscrowsFromIndexerResponse;
41
+ userRolesInEscrow: string[];
42
+ activeRole: Role[];
43
+ evidenceVisibleMap: Record<number, boolean>;
44
+ setEvidenceVisibleMap: React.Dispatch<
45
+ React.SetStateAction<Record<number, boolean>>
46
+ >;
47
+ }
48
+
49
+ export const MilestoneDetailDialog = ({
50
+ isOpen,
51
+ onClose,
52
+ selectedMilestone,
53
+ }: MilestoneDetailDialogProps) => {
54
+ const getMilestoneStatusBadge = (
55
+ milestone: SingleReleaseMilestone | MultiReleaseMilestone
56
+ ) => {
57
+ if ("flags" in milestone && milestone.flags?.disputed) {
58
+ return (
59
+ <Badge variant="destructive">
60
+ <CircleAlert className="h-3.5 w-3.5" />
61
+ <span>Disputed</span>
62
+ </Badge>
63
+ );
64
+ }
65
+ if ("flags" in milestone && milestone.flags?.released) {
66
+ return (
67
+ <Badge variant="default">
68
+ <CircleCheckBig className="h-3.5 w-3.5" />
69
+ <span>Released</span>
70
+ </Badge>
71
+ );
72
+ }
73
+ if (
74
+ "flags" in milestone &&
75
+ milestone.flags?.resolved &&
76
+ !milestone.flags?.disputed
77
+ ) {
78
+ return (
79
+ <Badge variant="default">
80
+ <Handshake className="h-3.5 w-3.5" />
81
+ <span>Resolved</span>
82
+ </Badge>
83
+ );
84
+ }
85
+ if (
86
+ ("flags" in milestone && milestone.flags?.approved) ||
87
+ ("approved" in milestone && milestone.approved)
88
+ ) {
89
+ return (
90
+ <Badge variant="default">
91
+ <CheckCheck className="h-3.5 w-3.5" />
92
+ <span>Approved</span>
93
+ </Badge>
94
+ );
95
+ }
96
+ return (
97
+ <Badge variant="outline">
98
+ <Layers className="h-3.5 w-3.5" />
99
+ <span className="uppercase">
100
+ {milestone.status
101
+ ? milestone.status.match(/[a-z][A-Z]/)
102
+ ? milestone.status.replace(/([A-Z])/g, " $1").toLowerCase()
103
+ : milestone.status.toLowerCase()
104
+ : ""}
105
+ </span>
106
+ </Badge>
107
+ );
108
+ };
109
+
110
+ const isValidUrl = (url: string) => {
111
+ try {
112
+ new URL(url);
113
+ return true;
114
+ } catch (error) {
115
+ return false;
116
+ }
117
+ };
118
+
119
+ if (!selectedMilestone) return null;
120
+
121
+ return (
122
+ <Dialog open={isOpen} onOpenChange={onClose}>
123
+ <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
124
+ <DialogHeader className="pb-4 border-b border-border">
125
+ <div className="flex items-center gap-3">
126
+ <div className="flex items-center justify-center w-10 h-10 bg-primary/10 rounded-full">
127
+ <Hash className="w-5 h-5 text-primary" />
128
+ </div>
129
+ <div>
130
+ <DialogTitle className="text-xl font-semibold truncate">
131
+ {selectedMilestone.milestone.description}
132
+ </DialogTitle>
133
+ <p className="text-sm text-muted-foreground">
134
+ Detailed information about this milestone
135
+ </p>
136
+ </div>
137
+ </div>
138
+ </DialogHeader>
139
+
140
+ <div className="space-y-6 pt-4">
141
+ <div className="flex items-center justify-between p-4 rounded-lg border border-border">
142
+ <div className="flex items-center gap-3">
143
+ <div className="flex items-center justify-center w-8 h-8 bg-background rounded-full shadow-sm border border-border">
144
+ <Calendar className="w-4 h-4 text-primary" />
145
+ </div>
146
+ <div>
147
+ <p className="text-sm font-medium text-foreground">
148
+ Current Status
149
+ </p>
150
+ <p className="text-xs text-muted-foreground">
151
+ Milestone progress
152
+ </p>
153
+ </div>
154
+ </div>
155
+ {getMilestoneStatusBadge(selectedMilestone.milestone)}
156
+ </div>
157
+
158
+ <Card className="border border-border shadow-sm">
159
+ <CardHeader className="pb-3">
160
+ <CardTitle className="text-lg flex items-center gap-2">
161
+ <div className="w-1 h-6 bg-primary rounded-full"></div>
162
+ Basic Information
163
+ </CardTitle>
164
+ </CardHeader>
165
+ <CardContent className="space-y-4">
166
+ <div className="space-y-2">
167
+ <label className="text-sm font-medium text-foreground flex items-center gap-2">
168
+ <FileCheck2 className="w-4 h-4 text-muted-foreground" />
169
+ Description
170
+ </label>
171
+ <p className="text-sm text-muted-foreground bg-muted/50 p-3 rounded-md border-l-4 border-primary/20">
172
+ {selectedMilestone.milestone.description}
173
+ </p>
174
+ </div>
175
+
176
+ {"amount" in selectedMilestone.milestone && (
177
+ <div className="space-y-2">
178
+ <label className="text-sm font-medium text-foreground flex items-center gap-2">
179
+ <DollarSign className="w-4 h-4 text-green-600 dark:text-green-400" />
180
+ Amount
181
+ </label>
182
+ <div className="flex items-center gap-2 bg-green-500/10 dark:bg-green-400/10 p-3 rounded-md border-l-4 border-green-500/20 dark:border-green-400/20">
183
+ <DollarSign className="w-5 h-5 text-green-600 dark:text-green-400" />
184
+ <span className="text-lg font-bold text-green-700 dark:text-green-300">
185
+ {selectedMilestone.milestone.amount}
186
+ </span>
187
+ </div>
188
+ </div>
189
+ )}
190
+ </CardContent>
191
+ </Card>
192
+
193
+ {selectedMilestone.milestone.evidence && (
194
+ <Card className="border border-border shadow-sm">
195
+ <CardHeader className="pb-3">
196
+ <CardTitle className="text-lg flex items-center gap-2">
197
+ <div className="w-1 h-6 bg-green-600 dark:bg-green-400 rounded-full"></div>
198
+ Evidence
199
+ </CardTitle>
200
+ </CardHeader>
201
+ <CardContent>
202
+ <div className="space-y-2">
203
+ <label className="text-sm font-medium text-foreground flex items-center gap-2">
204
+ <FileCheck2 className="w-4 h-4 text-green-600 dark:text-green-400" />
205
+ Evidence URL
206
+ </label>
207
+ <div className="bg-muted/50 p-3 rounded-md border-l-4 border-green-500/20 dark:border-green-400/20">
208
+ {(() => {
209
+ const result = isValidUrl(
210
+ selectedMilestone.milestone.evidence
211
+ );
212
+ return (
213
+ <div className="flex items-center justify-between">
214
+ <span className="text-sm text-muted-foreground break-all pr-2">
215
+ {selectedMilestone.milestone.evidence}
216
+ </span>
217
+ {result && (
218
+ <Link
219
+ href={selectedMilestone.milestone.evidence}
220
+ target="_blank"
221
+ rel="noopener noreferrer"
222
+ >
223
+ <Button
224
+ size="sm"
225
+ variant="outline"
226
+ className="shrink-0"
227
+ >
228
+ <ExternalLink className="w-4 h-4 mr-1" />
229
+ Open
230
+ </Button>
231
+ </Link>
232
+ )}
233
+ </div>
234
+ );
235
+ })()}
236
+ </div>
237
+ </div>
238
+ </CardContent>
239
+ </Card>
240
+ )}
241
+
242
+ {"disputeStartedBy" in selectedMilestone.milestone && (
243
+ <Card className="border border-border shadow-sm">
244
+ <CardHeader className="pb-3">
245
+ <CardTitle className="text-lg flex items-center gap-2">
246
+ <div className="w-1 h-6 bg-red-600 dark:bg-red-400 rounded-full"></div>
247
+ Dispute Information
248
+ </CardTitle>
249
+ </CardHeader>
250
+ <CardContent>
251
+ <div className="flex items-center gap-3 bg-red-500/10 dark:bg-red-400/10 p-3 rounded-md border-l-4 border-red-500/20 dark:border-red-400/20">
252
+ <User className="w-5 h-5 text-red-600 dark:text-red-400" />
253
+ <div>
254
+ <p className="text-sm font-medium text-red-700 dark:text-red-300">
255
+ <span className="font-bold">Disputed by:</span>{" "}
256
+ {selectedMilestone.milestone.disputeStartedBy ===
257
+ "serviceProvider"
258
+ ? "Service Provider"
259
+ : "Approver"}
260
+ </p>
261
+ {"flags" in selectedMilestone.milestone &&
262
+ !selectedMilestone.milestone.flags?.resolved && (
263
+ <p className="text-xs text-red-600 dark:text-red-400">
264
+ This milestone is currently under dispute
265
+ </p>
266
+ )}
267
+ </div>
268
+ </div>
269
+ </CardContent>
270
+ </Card>
271
+ )}
272
+ </div>
273
+ </DialogContent>
274
+ </Dialog>
275
+ );
276
+ };
@@ -0,0 +1,87 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import { GetEscrowsFromIndexerResponse as Escrow } from "@trustless-work/escrow/types";
5
+ import {
6
+ MultiReleaseMilestone,
7
+ Role,
8
+ SingleReleaseMilestone,
9
+ } from "@trustless-work/escrow";
10
+ import { MilestoneCard } from "./MilestoneCard";
11
+ import { MilestoneDetailDialog } from "./MilestoneDetailDialog";
12
+
13
+ interface MilestonesProps {
14
+ selectedEscrow: Escrow;
15
+ userRolesInEscrow: string[];
16
+ setEvidenceVisibleMap: React.Dispatch<
17
+ React.SetStateAction<Record<number, boolean>>
18
+ >;
19
+ evidenceVisibleMap: Record<number, boolean>;
20
+ activeRole: Role[];
21
+ }
22
+
23
+ export const Milestones = ({
24
+ selectedEscrow,
25
+ userRolesInEscrow,
26
+ setEvidenceVisibleMap,
27
+ evidenceVisibleMap,
28
+ activeRole,
29
+ }: MilestonesProps) => {
30
+ const [selectedMilestoneForDetail, setSelectedMilestoneForDetail] = useState<{
31
+ milestone: SingleReleaseMilestone | MultiReleaseMilestone;
32
+ index: number;
33
+ } | null>(null);
34
+
35
+ const handleViewDetails = useCallback(
36
+ (
37
+ milestone: SingleReleaseMilestone | MultiReleaseMilestone,
38
+ index: number
39
+ ) => {
40
+ setSelectedMilestoneForDetail({
41
+ milestone,
42
+ index,
43
+ });
44
+ },
45
+ []
46
+ );
47
+
48
+ return (
49
+ <div className="flex w-full">
50
+ <div className="flex flex-col gap-6 w-full">
51
+ <div className="flex w-full justify-between items-center">
52
+ <label
53
+ htmlFor="milestones"
54
+ className="flex items-center gap-2 text-lg font-medium"
55
+ >
56
+ Milestones
57
+ </label>
58
+ </div>
59
+
60
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-4">
61
+ {selectedEscrow.milestones.map((milestone, milestoneIndex) => (
62
+ <MilestoneCard
63
+ key={`milestone-${milestoneIndex}-${milestone.description}-${milestone.status}`}
64
+ milestone={milestone}
65
+ milestoneIndex={milestoneIndex}
66
+ selectedEscrow={selectedEscrow}
67
+ userRolesInEscrow={userRolesInEscrow}
68
+ activeRole={activeRole}
69
+ onViewDetails={handleViewDetails}
70
+ />
71
+ ))}
72
+ </div>
73
+
74
+ <MilestoneDetailDialog
75
+ isOpen={!!selectedMilestoneForDetail}
76
+ onClose={() => setSelectedMilestoneForDetail(null)}
77
+ selectedMilestone={selectedMilestoneForDetail}
78
+ selectedEscrow={selectedEscrow}
79
+ userRolesInEscrow={userRolesInEscrow}
80
+ activeRole={activeRole}
81
+ evidenceVisibleMap={evidenceVisibleMap}
82
+ setEvidenceVisibleMap={setEvidenceVisibleMap}
83
+ />
84
+ </div>
85
+ </div>
86
+ );
87
+ };
@@ -0,0 +1,191 @@
1
+ import { GetEscrowsFromIndexerResponse } from "@trustless-work/escrow/types";
2
+ import { cn } from "@/lib/utils";
3
+ import {
4
+ MultiReleaseMilestone,
5
+ SingleReleaseMilestone,
6
+ } from "@trustless-work/escrow";
7
+
8
+ type Escrow = {
9
+ [K in keyof Omit<
10
+ GetEscrowsFromIndexerResponse,
11
+ "type" | "updatedAt" | "createdAt" | "user"
12
+ >]: K extends "trustline"
13
+ ? Omit<NonNullable<GetEscrowsFromIndexerResponse["trustline"]>, "name">
14
+ : GetEscrowsFromIndexerResponse[K];
15
+ };
16
+
17
+ interface ProgressEscrowProps {
18
+ escrow: Escrow;
19
+ className?: string;
20
+ }
21
+
22
+ const ProgressCircle = ({
23
+ percentage,
24
+ color,
25
+ size = 44,
26
+ strokeWidth = 3,
27
+ }: {
28
+ percentage: number;
29
+ color: string;
30
+ size?: number;
31
+ strokeWidth?: number;
32
+ }) => {
33
+ const radius = (size - strokeWidth) / 2;
34
+ const circumference = 2 * Math.PI * radius;
35
+ const strokeDashoffset = circumference - (percentage / 100) * circumference;
36
+
37
+ return (
38
+ <div className="relative inline-flex">
39
+ <svg
40
+ width={size}
41
+ height={size}
42
+ viewBox={`0 0 ${size} ${size}`}
43
+ className="transform -rotate-90"
44
+ >
45
+ <circle
46
+ cx={size / 2}
47
+ cy={size / 2}
48
+ r={radius}
49
+ fill="none"
50
+ stroke="rgba(226, 232, 240, 0.3)"
51
+ strokeWidth={strokeWidth}
52
+ />
53
+ <circle
54
+ cx={size / 2}
55
+ cy={size / 2}
56
+ r={radius}
57
+ fill="none"
58
+ stroke={color}
59
+ strokeWidth={strokeWidth}
60
+ strokeDasharray={circumference}
61
+ strokeDashoffset={strokeDashoffset}
62
+ strokeLinecap="round"
63
+ />
64
+ </svg>
65
+ <div className="absolute inset-0 flex items-center justify-center text-xs font-medium">
66
+ {Math.round(percentage)}%
67
+ </div>
68
+ </div>
69
+ );
70
+ };
71
+
72
+ const ProgressEscrow = ({ escrow, className }: ProgressEscrowProps) => {
73
+ const completedMilestones = escrow.milestones.filter(
74
+ (milestone) => milestone.status === "completed"
75
+ ).length;
76
+ const approvedMilestones = escrow.milestones.filter(
77
+ (milestone: SingleReleaseMilestone | MultiReleaseMilestone) =>
78
+ ("flags" in milestone && milestone.flags?.approved === true) ||
79
+ (!("flags" in milestone) &&
80
+ (milestone as SingleReleaseMilestone).approved === true)
81
+ ).length;
82
+ const disputedMilestones = escrow.milestones.filter(
83
+ (milestone: SingleReleaseMilestone | MultiReleaseMilestone) =>
84
+ "flags" in milestone && milestone.flags?.disputed === true
85
+ ).length;
86
+ const resolvedMilestones = escrow.milestones.filter(
87
+ (milestone: SingleReleaseMilestone | MultiReleaseMilestone) =>
88
+ "flags" in milestone && milestone.flags?.resolved === true
89
+ ).length;
90
+ const releasedMilestones = escrow.milestones.filter(
91
+ (milestone: SingleReleaseMilestone | MultiReleaseMilestone) =>
92
+ "flags" in milestone && milestone.flags?.released === true
93
+ ).length;
94
+ const totalMilestones = escrow.milestones.length;
95
+
96
+ const progressPercentageCompleted =
97
+ totalMilestones > 0 ? (completedMilestones / totalMilestones) * 100 : 0;
98
+ const progressPercentageApproved =
99
+ totalMilestones > 0 ? (approvedMilestones / totalMilestones) * 100 : 0;
100
+ const progressPercentageDisputed =
101
+ totalMilestones > 0 ? (disputedMilestones / totalMilestones) * 100 : 0;
102
+ const progressPercentageResolved =
103
+ totalMilestones > 0 ? (resolvedMilestones / totalMilestones) * 100 : 0;
104
+ const progressPercentageReleased =
105
+ totalMilestones > 0 ? (releasedMilestones / totalMilestones) * 100 : 0;
106
+
107
+ const shouldHideProgress = escrow.flags?.released || escrow.flags?.resolved;
108
+ const isMultiRelease =
109
+ escrow.milestones[0] && "flags" in escrow.milestones[0];
110
+
111
+ if (shouldHideProgress || totalMilestones === 0) {
112
+ return null;
113
+ }
114
+
115
+ return (
116
+ <div className={cn("space-y-4 px-10 w-full", className)}>
117
+ <div className="flex flex-wrap items-center justify-center gap-6">
118
+ <div className="flex items-center gap-3">
119
+ <ProgressCircle
120
+ percentage={progressPercentageCompleted}
121
+ color="#006be4"
122
+ />
123
+ <div className="text-xs">
124
+ <div className="font-medium">Completed</div>
125
+ <div className="text-muted-foreground">
126
+ {completedMilestones}/{totalMilestones}
127
+ </div>
128
+ </div>
129
+ </div>
130
+
131
+ <div className="flex items-center gap-3">
132
+ <ProgressCircle
133
+ percentage={progressPercentageApproved}
134
+ color="#15803d"
135
+ />
136
+ <div className="text-xs">
137
+ <div className="font-medium">Approved</div>
138
+ <div className="text-muted-foreground">
139
+ {approvedMilestones}/{totalMilestones}
140
+ </div>
141
+ </div>
142
+ </div>
143
+
144
+ {isMultiRelease && (
145
+ <>
146
+ <div className="flex items-center gap-3">
147
+ <ProgressCircle
148
+ percentage={progressPercentageDisputed}
149
+ color="#dc2626"
150
+ />
151
+ <div className="text-xs">
152
+ <div className="font-medium">Disputed</div>
153
+ <div className="text-muted-foreground">
154
+ {disputedMilestones}/{totalMilestones}
155
+ </div>
156
+ </div>
157
+ </div>
158
+
159
+ <div className="flex items-center gap-3">
160
+ <ProgressCircle
161
+ percentage={progressPercentageResolved}
162
+ color="#15803d"
163
+ />
164
+ <div className="text-xs">
165
+ <div className="font-medium">Resolved</div>
166
+ <div className="text-muted-foreground">
167
+ {resolvedMilestones}/{totalMilestones}
168
+ </div>
169
+ </div>
170
+ </div>
171
+
172
+ <div className="flex items-center gap-3">
173
+ <ProgressCircle
174
+ percentage={progressPercentageReleased}
175
+ color="#15803d"
176
+ />
177
+ <div className="text-xs">
178
+ <div className="font-medium">Released</div>
179
+ <div className="text-muted-foreground">
180
+ {releasedMilestones}/{totalMilestones}
181
+ </div>
182
+ </div>
183
+ </div>
184
+ </>
185
+ )}
186
+ </div>
187
+ </div>
188
+ );
189
+ };
190
+
191
+ export default ProgressEscrow;
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import type { ReactNode } from "react";
5
+ import type { LucideIcon } from "lucide-react";
6
+ import { cn } from "@/lib/utils";
7
+ import { Button } from "__UI_BASE__/button";
8
+ import { Card, CardContent } from "__UI_BASE__/card";
9
+ import { Badge } from "__UI_BASE__/badge";
10
+ import { formatText } from "@/components/tw-blocks/helpers/format.helper";
11
+
12
+ interface StatisticsCardProps {
13
+ title: string;
14
+ icon: LucideIcon;
15
+ iconColor?: string;
16
+ value: ReactNode;
17
+ subValue?: ReactNode;
18
+ actionLabel?: string;
19
+ onAction?: () => void;
20
+ className?: string;
21
+ iconSize?: number;
22
+ fundedBy?: string;
23
+ }
24
+
25
+ export const StatisticsCard = ({
26
+ title,
27
+ icon: Icon,
28
+ iconColor,
29
+ value,
30
+ subValue,
31
+ actionLabel,
32
+ onAction,
33
+ className,
34
+ iconSize = 30,
35
+ fundedBy,
36
+ }: StatisticsCardProps) => {
37
+ return (
38
+ <Card
39
+ className={cn(
40
+ "overflow-hidden cursor-pointer hover:shadow-lg w-full py-1",
41
+ className
42
+ )}
43
+ >
44
+ <CardContent className="py-4 px-8 min-h-20">
45
+ <div className="flex items-center justify-between">
46
+ <div className="flex">
47
+ <p className="text-sm font-medium text-muted-foreground">{title}</p>
48
+ </div>
49
+
50
+ <Icon className={iconColor} size={iconSize} />
51
+ </div>
52
+ <div className="mt-2 flex items-baseline justify-between">
53
+ <div>
54
+ <h3 className="text-2xl font-semibold">{value}</h3>
55
+ {subValue}
56
+ </div>
57
+ {fundedBy && (
58
+ <Badge
59
+ variant="outline"
60
+ className="text-xs text-muted-foreground uppercase"
61
+ >
62
+ Funded by {formatText(fundedBy)}
63
+ </Badge>
64
+ )}
65
+ {actionLabel && onAction && (
66
+ <Button
67
+ variant="link"
68
+ type="button"
69
+ onClick={onAction}
70
+ className="text-xs text-muted-foreground my-0 p-0 h-auto"
71
+ >
72
+ {actionLabel}
73
+ </Button>
74
+ )}
75
+ </div>
76
+ </CardContent>
77
+ </Card>
78
+ );
79
+ };