@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.
- package/README.md +96 -0
- package/bin/index.js +1123 -0
- package/package.json +44 -0
- package/templates/deps.json +29 -0
- package/templates/escrows/details/Actions.tsx +149 -0
- package/templates/escrows/details/Entities.tsx +48 -0
- package/templates/escrows/details/EntityCard.tsx +98 -0
- package/templates/escrows/details/EscrowDetailDialog.tsx +154 -0
- package/templates/escrows/details/GeneralInformation.tsx +329 -0
- package/templates/escrows/details/MilestoneCard.tsx +254 -0
- package/templates/escrows/details/MilestoneDetailDialog.tsx +276 -0
- package/templates/escrows/details/Milestones.tsx +87 -0
- package/templates/escrows/details/ProgressEscrow.tsx +191 -0
- package/templates/escrows/details/StatisticsCard.tsx +79 -0
- package/templates/escrows/details/SuccessReleaseDialog.tsx +101 -0
- package/templates/escrows/details/useDetailsEscrow.ts +126 -0
- package/templates/escrows/escrow-context/EscrowAmountProvider.tsx +86 -0
- package/templates/escrows/escrow-context/EscrowDialogsProvider.tsx +108 -0
- package/templates/escrows/escrow-context/EscrowProvider.tsx +124 -0
- package/templates/escrows/escrows-by-role/cards/EscrowsCards.tsx +503 -0
- package/templates/escrows/escrows-by-role/cards/Filters.tsx +421 -0
- package/templates/escrows/escrows-by-role/table/EscrowsTable.tsx +427 -0
- package/templates/escrows/escrows-by-role/table/Filters.tsx +421 -0
- package/templates/escrows/escrows-by-role/useEscrowsByRole.shared.ts +336 -0
- package/templates/escrows/escrows-by-signer/cards/EscrowsCards.tsx +502 -0
- package/templates/escrows/escrows-by-signer/cards/Filters.tsx +389 -0
- package/templates/escrows/escrows-by-signer/table/EscrowsTable.tsx +422 -0
- package/templates/escrows/escrows-by-signer/table/Filters.tsx +389 -0
- package/templates/escrows/escrows-by-signer/useEscrowsBySigner.shared.ts +320 -0
- package/templates/escrows/single-release/approve-milestone/button/ApproveMilestone.tsx +78 -0
- package/templates/escrows/single-release/approve-milestone/dialog/ApproveMilestone.tsx +102 -0
- package/templates/escrows/single-release/approve-milestone/form/ApproveMilestone.tsx +80 -0
- package/templates/escrows/single-release/approve-milestone/shared/schema.ts +9 -0
- package/templates/escrows/single-release/approve-milestone/shared/useApproveMilestone.ts +67 -0
- package/templates/escrows/single-release/change-milestone-status/button/ChangeMilestoneStatus.tsx +78 -0
- package/templates/escrows/single-release/change-milestone-status/dialog/ChangeMilestoneStatus.tsx +167 -0
- package/templates/escrows/single-release/change-milestone-status/form/ChangeMilestoneStatus.tsx +114 -0
- package/templates/escrows/single-release/change-milestone-status/shared/schema.ts +15 -0
- package/templates/escrows/single-release/change-milestone-status/shared/useChangeMilestoneStatus.ts +77 -0
- package/templates/escrows/single-release/dispute-escrow/button/DisputeEscrow.tsx +68 -0
- package/templates/escrows/single-release/fund-escrow/button/FundEscrow.tsx +84 -0
- package/templates/escrows/single-release/fund-escrow/dialog/FundEscrow.tsx +77 -0
- package/templates/escrows/single-release/fund-escrow/form/FundEscrow.tsx +54 -0
- package/templates/escrows/single-release/fund-escrow/shared/schema.ts +10 -0
- package/templates/escrows/single-release/fund-escrow/shared/useFundEscrow.ts +66 -0
- package/templates/escrows/single-release/initialize-escrow/dialog/InitializeEscrow.tsx +526 -0
- package/templates/escrows/single-release/initialize-escrow/form/InitializeEscrow.tsx +504 -0
- package/templates/escrows/single-release/initialize-escrow/shared/schema.ts +232 -0
- package/templates/escrows/single-release/initialize-escrow/shared/useInitializeEscrow.ts +115 -0
- package/templates/escrows/single-release/release-escrow/button/ReleaseEscrow.tsx +80 -0
- package/templates/escrows/single-release/resolve-dispute/button/ResolveDispute.tsx +94 -0
- package/templates/escrows/single-release/resolve-dispute/dialog/ResolveDispute.tsx +123 -0
- package/templates/escrows/single-release/resolve-dispute/form/ResolveDispute.tsx +82 -0
- package/templates/escrows/single-release/resolve-dispute/shared/schema.ts +82 -0
- package/templates/escrows/single-release/resolve-dispute/shared/useResolveDispute.ts +58 -0
- package/templates/escrows/single-release/update-escrow/dialog/UpdateEscrow.tsx +485 -0
- package/templates/escrows/single-release/update-escrow/form/UpdateEscrow.tsx +463 -0
- package/templates/escrows/single-release/update-escrow/shared/schema.ts +139 -0
- package/templates/escrows/single-release/update-escrow/shared/useUpdateEscrow.ts +211 -0
- package/templates/handle-errors/errors.enum.ts +6 -0
- package/templates/handle-errors/handle.ts +47 -0
- package/templates/helpers/format.helper.ts +27 -0
- package/templates/helpers/useCopy.ts +13 -0
- package/templates/providers/ReactQueryClientProvider.tsx +28 -0
- package/templates/providers/TrustlessWork.tsx +30 -0
- package/templates/tanstak/useEscrowsByRoleQuery.ts +87 -0
- package/templates/tanstak/useEscrowsBySignerQuery.ts +78 -0
- package/templates/tanstak/useEscrowsMutations.ts +411 -0
- package/templates/wallet-kit/WalletButtons.tsx +116 -0
- package/templates/wallet-kit/WalletProvider.tsx +94 -0
- package/templates/wallet-kit/trustlines.ts +40 -0
- package/templates/wallet-kit/useWallet.ts +77 -0
- package/templates/wallet-kit/validators.ts +12 -0
- 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
|
+
};
|