@trustless-work/blocks 1.0.2 → 1.0.4

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/bin/index.js CHANGED
@@ -1580,7 +1580,7 @@ if (args[0] === "init") {
1580
1580
  }
1581
1581
 
1582
1582
  const addShadcn = await promptYesNo(
1583
- "Add shadcn components (button, input, form, card, sonner, checkbox, dialog, textarea, sonner, select, table, calendar, popover, separator, calendar-05, badge, sheet, tabs, avatar, tooltip, progress)?",
1583
+ "Add shadcn components (button, input, form, card, sonner, checkbox, dialog, textarea, sonner, select, table, calendar, popover, separator, calendar-05, badge, sheet, tabs, avatar, tooltip, progress, chart)?",
1584
1584
  true
1585
1585
  );
1586
1586
  if (addShadcn) {
@@ -1609,6 +1609,7 @@ if (args[0] === "init") {
1609
1609
  "avatar",
1610
1610
  "tooltip",
1611
1611
  "progress",
1612
+ "chart",
1612
1613
  ]);
1613
1614
  });
1614
1615
  } else {
@@ -1621,7 +1622,7 @@ if (args[0] === "init") {
1621
1622
  }
1622
1623
  const meta = JSON.parse(fs.readFileSync(GLOBAL_DEPS_FILE, "utf8"));
1623
1624
  const installLibs = await promptYesNo(
1624
- "Install (react-hook-form, @tanstack/react-query, @tanstack/react-query-devtools, @trustless-work/escrow, @hookform/resolvers, axios, @creit.tech/stellar-wallets-kit, react-day-picker & zod) dependencies now?",
1625
+ "Install (react-hook-form, @tanstack/react-query, @tanstack/react-query-devtools, @trustless-work/escrow, @hookform/resolvers, axios, @creit.tech/stellar-wallets-kit, react-day-picker, recharts & zod) dependencies now?",
1625
1626
  true
1626
1627
  );
1627
1628
  if (installLibs) {
@@ -1807,6 +1808,10 @@ if (args[0] === "init") {
1807
1808
  --- Escrows ---
1808
1809
  trustless-work add escrows
1809
1810
 
1811
+ --- Dashboard ---
1812
+ trustless-work add dashboard
1813
+ trustless-work add dashboard/dashboard-01
1814
+
1810
1815
  --- Indicators ---
1811
1816
  trustless-work add escrows/indicators/balance-progress
1812
1817
  trustless-work add escrows/indicators/balance-progress/bar
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trustless-work/blocks",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "author": "Trustless Work",
5
5
  "keywords": [
6
6
  "react",
@@ -0,0 +1,268 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import {
5
+ Card,
6
+ CardContent,
7
+ CardDescription,
8
+ CardHeader,
9
+ CardTitle,
10
+ } from "__UI_BASE__/card";
11
+ import { Separator } from "__UI_BASE__/separator";
12
+ import { useDashboard } from "./useDashboard";
13
+ import { formatCurrency } from "../../helpers/format.helper";
14
+ import { Activity, Layers3, PiggyBank } from "lucide-react";
15
+ import {
16
+ AreaChart,
17
+ Area,
18
+ XAxis,
19
+ CartesianGrid,
20
+ BarChart,
21
+ Bar,
22
+ PieChart,
23
+ Pie,
24
+ Cell,
25
+ } from "recharts";
26
+ import {
27
+ ChartContainer,
28
+ ChartTooltip,
29
+ ChartTooltipContent,
30
+ type ChartConfig,
31
+ } from "__UI_BASE__/chart";
32
+
33
+ const chartConfigBar: ChartConfig = {
34
+ desktop: {
35
+ label: "Amount",
36
+ color: "var(--chart-1)",
37
+ },
38
+ };
39
+
40
+ const chartConfigDonut: ChartConfig = {
41
+ visitors: { label: "Count" },
42
+ single: { label: "Single", color: "var(--chart-1)" },
43
+ multi: { label: "Multi", color: "var(--chart-2)" },
44
+ };
45
+
46
+ const chartConfigArea: ChartConfig = {
47
+ desktop: {
48
+ label: "Created",
49
+ color: "var(--chart-1)",
50
+ },
51
+ };
52
+
53
+ export function Dashboard() {
54
+ const {
55
+ isLoading,
56
+ totalEscrows,
57
+ totalAmount,
58
+ totalBalance,
59
+ typeSlices,
60
+ amountsByDate,
61
+ createdByDate,
62
+ } = useDashboard();
63
+
64
+ const barData = React.useMemo(
65
+ () => amountsByDate.map((d) => ({ month: d.date, desktop: d.amount })),
66
+ [amountsByDate]
67
+ );
68
+
69
+ const donutData = React.useMemo(
70
+ () =>
71
+ typeSlices.map((s) => ({
72
+ browser: s.type === "single" ? "single" : "multi",
73
+ visitors: s.value,
74
+ fill:
75
+ s.type === "single" ? "var(--color-single)" : "var(--color-multi)",
76
+ })),
77
+ [typeSlices]
78
+ );
79
+
80
+ const areaData = React.useMemo(
81
+ () => createdByDate.map((d) => ({ month: d.date, desktop: d.count })),
82
+ [createdByDate]
83
+ );
84
+
85
+ return (
86
+ <div className="grid gap-6">
87
+ {/* KPI Cards */}
88
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
89
+ <Card>
90
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
91
+ <CardTitle className="text-sm font-medium">Escrows</CardTitle>
92
+ <Layers3 className="h-4 w-4 text-muted-foreground" />
93
+ </CardHeader>
94
+ <CardContent>
95
+ <div className="text-2xl font-bold">
96
+ {isLoading ? "-" : totalEscrows}
97
+ </div>
98
+ <p className="text-xs text-muted-foreground">
99
+ Total number of escrows
100
+ </p>
101
+ </CardContent>
102
+ </Card>
103
+
104
+ <Card>
105
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
106
+ <CardTitle className="text-sm font-medium">Total Amount</CardTitle>
107
+ <Activity className="h-4 w-4 text-muted-foreground" />
108
+ </CardHeader>
109
+ <CardContent>
110
+ <div className="text-2xl font-bold">
111
+ {isLoading ? "-" : formatCurrency(totalAmount, "USDC")}
112
+ </div>
113
+ <p className="text-xs text-muted-foreground">
114
+ Sum of amounts (SR + MR)
115
+ </p>
116
+ </CardContent>
117
+ </Card>
118
+
119
+ <Card>
120
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
121
+ <CardTitle className="text-sm font-medium">Total Balance</CardTitle>
122
+ <PiggyBank className="h-4 w-4 text-muted-foreground" />
123
+ </CardHeader>
124
+ <CardContent>
125
+ <div className="text-2xl font-bold">
126
+ {isLoading ? "-" : formatCurrency(totalBalance, "USDC")}
127
+ </div>
128
+ <p className="text-xs text-muted-foreground">
129
+ Total balance across all escrows
130
+ </p>
131
+ </CardContent>
132
+ </Card>
133
+ </div>
134
+
135
+ <Separator />
136
+
137
+ {/* Charts */}
138
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
139
+ {/* Bar chart: Amounts by date (shadcn pattern) */}
140
+ <Card>
141
+ <CardHeader>
142
+ <CardTitle>Escrow Amounts</CardTitle>
143
+ <CardDescription>Amounts by date</CardDescription>
144
+ </CardHeader>
145
+ <CardContent>
146
+ <ChartContainer
147
+ className="w-full h-56 sm:h-64 lg:h-72"
148
+ config={chartConfigBar}
149
+ >
150
+ <BarChart accessibilityLayer data={barData}>
151
+ <CartesianGrid vertical={false} />
152
+ <XAxis
153
+ dataKey="month"
154
+ tickLine={false}
155
+ tickMargin={10}
156
+ axisLine={false}
157
+ tickFormatter={(value) =>
158
+ new Date(String(value)).toLocaleDateString("en-US", {
159
+ month: "short",
160
+ day: "numeric",
161
+ })
162
+ }
163
+ />
164
+ <ChartTooltip
165
+ cursor={false}
166
+ content={<ChartTooltipContent hideLabel />}
167
+ />
168
+ <Bar dataKey="desktop" fill="var(--color-desktop)" radius={8} />
169
+ </BarChart>
170
+ </ChartContainer>
171
+ </CardContent>
172
+ </Card>
173
+
174
+ {/* Donut chart: Escrow types (shadcn pattern) */}
175
+ <Card className="flex flex-col">
176
+ <CardHeader className="items-center pb-0">
177
+ <CardTitle>Escrow Types</CardTitle>
178
+ <CardDescription>Escrow types</CardDescription>
179
+ </CardHeader>
180
+ <CardContent className="flex-1 pb-0">
181
+ <ChartContainer
182
+ config={chartConfigDonut}
183
+ className="w-full h-56 sm:h-64 lg:h-72"
184
+ >
185
+ <PieChart>
186
+ <ChartTooltip
187
+ cursor={false}
188
+ content={<ChartTooltipContent hideLabel />}
189
+ />
190
+ <Pie
191
+ data={donutData}
192
+ dataKey="visitors"
193
+ nameKey="browser"
194
+ innerRadius={60}
195
+ >
196
+ {donutData.map((slice, idx) => (
197
+ <Cell key={`cell-${idx}`} fill={slice.fill} />
198
+ ))}
199
+ </Pie>
200
+ </PieChart>
201
+ </ChartContainer>
202
+ <div className="mt-4 flex items-center justify-center gap-6">
203
+ <div className="flex items-center gap-2">
204
+ <span
205
+ className="h-2 w-2 rounded-full"
206
+ style={{ background: "var(--chart-1)" }}
207
+ />
208
+ <span className="text-xs text-muted-foreground">Single</span>
209
+ </div>
210
+ <div className="flex items-center gap-2">
211
+ <span
212
+ className="h-2 w-2 rounded-full"
213
+ style={{ background: "var(--chart-2)" }}
214
+ />
215
+ <span className="text-xs text-muted-foreground">Multi</span>
216
+ </div>
217
+ </div>
218
+ </CardContent>
219
+ </Card>
220
+
221
+ {/* Area chart: Created escrows (shadcn pattern) */}
222
+ <Card className="lg:col-span-2">
223
+ <CardHeader>
224
+ <CardTitle>Escrow Created</CardTitle>
225
+ <CardDescription>Created escrows by date</CardDescription>
226
+ </CardHeader>
227
+ <CardContent>
228
+ <ChartContainer
229
+ className="w-full h-56 sm:h-64 lg:h-72"
230
+ config={chartConfigArea}
231
+ >
232
+ <AreaChart
233
+ accessibilityLayer
234
+ data={areaData}
235
+ margin={{ left: 12, right: 12 }}
236
+ >
237
+ <CartesianGrid vertical={false} />
238
+ <XAxis
239
+ dataKey="month"
240
+ tickLine={false}
241
+ axisLine={false}
242
+ tickMargin={8}
243
+ tickFormatter={(value) =>
244
+ new Date(String(value)).toLocaleDateString("en-US", {
245
+ month: "short",
246
+ day: "numeric",
247
+ })
248
+ }
249
+ />
250
+ <ChartTooltip
251
+ cursor={false}
252
+ content={<ChartTooltipContent indicator="line" />}
253
+ />
254
+ <Area
255
+ dataKey="desktop"
256
+ type="natural"
257
+ fill="var(--color-desktop)"
258
+ fillOpacity={0.4}
259
+ stroke="var(--color-desktop)"
260
+ />
261
+ </AreaChart>
262
+ </ChartContainer>
263
+ </CardContent>
264
+ </Card>
265
+ </div>
266
+ </div>
267
+ );
268
+ }
@@ -0,0 +1,84 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { Tooltip as RechartsTooltip } from "recharts";
5
+
6
+ export type ChartConfig = Record<string, { label: string; color?: string }>;
7
+
8
+ interface ChartContainerProps {
9
+ config: ChartConfig;
10
+ className?: string;
11
+ children: React.ReactNode;
12
+ }
13
+
14
+ export function ChartContainer({
15
+ config,
16
+ className,
17
+ children,
18
+ }: ChartContainerProps) {
19
+ const style: React.CSSProperties = {};
20
+ for (const [key, value] of Object.entries(config)) {
21
+ const varName = `--color-${key}` as const;
22
+ if (value.color) (style as any)[varName] = value.color;
23
+ }
24
+ return (
25
+ <div className={className} style={style}>
26
+ {children}
27
+ </div>
28
+ );
29
+ }
30
+
31
+ type RechartsPayloadItem = {
32
+ value?: number | string;
33
+ dataKey?: string;
34
+ color?: string;
35
+ name?: string;
36
+ };
37
+
38
+ type RechartsTooltipContentProps = {
39
+ active?: boolean;
40
+ label?: string | number;
41
+ payload?: RechartsPayloadItem[];
42
+ };
43
+
44
+ export type ChartTooltipContentProps = {
45
+ hideLabel?: boolean;
46
+ indicator?: "line" | "dot";
47
+ } & RechartsTooltipContentProps;
48
+
49
+ export function ChartTooltip(
50
+ props: React.ComponentProps<typeof RechartsTooltip>
51
+ ) {
52
+ return <RechartsTooltip {...props} />;
53
+ }
54
+
55
+ export function ChartTooltipContent({
56
+ active,
57
+ label,
58
+ payload,
59
+ hideLabel,
60
+ }: ChartTooltipContentProps) {
61
+ if (!active || !payload || payload.length === 0) return null;
62
+ return (
63
+ <div className="rounded-md border bg-background p-2 text-sm shadow-sm">
64
+ {!hideLabel ? <div className="mb-1 font-medium">{label}</div> : null}
65
+ <div className="space-y-1">
66
+ {payload.map((item, idx) => (
67
+ <div key={idx} className="flex items-center gap-2">
68
+ <span
69
+ className="inline-block h-2 w-2 rounded-full"
70
+ style={{ backgroundColor: item.color }}
71
+ aria-hidden
72
+ />
73
+ <span className="text-muted-foreground">
74
+ {item.name ?? String(item.dataKey)}
75
+ </span>
76
+ <span className="font-medium ml-auto">
77
+ {item.value as React.ReactNode}
78
+ </span>
79
+ </div>
80
+ ))}
81
+ </div>
82
+ </div>
83
+ );
84
+ }
@@ -0,0 +1,119 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { useWalletContext } from "../../wallet-kit/WalletProvider";
5
+ import { useEscrowsBySignerQuery } from "../../tanstack/useEscrowsBySignerQuery";
6
+ import type { GetEscrowsFromIndexerResponse as Escrow } from "@trustless-work/escrow/types";
7
+
8
+ type AmountsByDatePoint = { date: string; amount: number };
9
+ type CreatedByDatePoint = { date: string; count: number };
10
+ type DonutSlice = { type: "single" | "multi"; value: number; fill: string };
11
+
12
+ function getCreatedDateKey(createdAt: Escrow["createdAt"]): string {
13
+ // createdAt is a Firestore-like timestamp: { _seconds, _nanoseconds }
14
+ const seconds = (createdAt as unknown as { _seconds?: number })?._seconds;
15
+ const d = seconds ? new Date(seconds * 1000) : new Date();
16
+ // YYYY-MM-DD
17
+ return d.toISOString().slice(0, 10);
18
+ }
19
+
20
+ function getSingleReleaseAmount(escrow: Escrow): number {
21
+ // Single release stores total in .amount
22
+ const raw = (escrow as unknown as { amount?: number | string }).amount;
23
+ const n = Number(raw ?? 0);
24
+ return Number.isFinite(n) ? n : 0;
25
+ }
26
+
27
+ function getMultiReleaseAmount(escrow: Escrow): number {
28
+ // Multi release accumulates across milestones
29
+ const milestones = (
30
+ escrow as unknown as {
31
+ milestones?: Array<{ amount?: number | string }>;
32
+ }
33
+ ).milestones;
34
+ if (!Array.isArray(milestones)) return 0;
35
+ return milestones.reduce((acc: number, m) => {
36
+ const n = Number(m?.amount ?? 0);
37
+ return acc + (Number.isFinite(n) ? n : 0);
38
+ }, 0);
39
+ }
40
+
41
+ function getEscrowAmount(escrow: Escrow): number {
42
+ if (escrow.type === "single-release") return getSingleReleaseAmount(escrow);
43
+ if (escrow.type === "multi-release") return getMultiReleaseAmount(escrow);
44
+ return 0;
45
+ }
46
+
47
+ export function useDashboard() {
48
+ const { walletAddress } = useWalletContext();
49
+
50
+ const {
51
+ data = [],
52
+ isLoading,
53
+ isFetching,
54
+ isError,
55
+ refetch,
56
+ } = useEscrowsBySignerQuery({
57
+ signer: walletAddress ?? "",
58
+ enabled: !!walletAddress,
59
+ });
60
+
61
+ const totalEscrows = React.useMemo<number>(() => data.length, [data.length]);
62
+
63
+ const totalAmount = React.useMemo<number>(() => {
64
+ return data.reduce((acc: number, e) => acc + getEscrowAmount(e), 0);
65
+ }, [data]);
66
+
67
+ const totalBalance = React.useMemo<number>(() => {
68
+ return data.reduce((acc: number, e) => acc + Number(e?.balance ?? 0), 0);
69
+ }, [data]);
70
+
71
+ const typeSlices = React.useMemo<DonutSlice[]>(() => {
72
+ let single = 0;
73
+ let multi = 0;
74
+ for (const e of data) {
75
+ if (e.type === "single-release") single += 1;
76
+ else if (e.type === "multi-release") multi += 1;
77
+ }
78
+ return [
79
+ { type: "single", value: single, fill: "var(--color-single)" },
80
+ { type: "multi", value: multi, fill: "var(--color-multi)" },
81
+ ];
82
+ }, [data]);
83
+
84
+ const amountsByDate = React.useMemo<AmountsByDatePoint[]>(() => {
85
+ const map = new Map<string, number>();
86
+ for (const e of data) {
87
+ const key = getCreatedDateKey(e.createdAt);
88
+ const current = map.get(key) ?? 0;
89
+ map.set(key, current + getEscrowAmount(e));
90
+ }
91
+ return Array.from(map.entries())
92
+ .map(([date, amount]) => ({ date, amount }))
93
+ .sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));
94
+ }, [data]);
95
+
96
+ const createdByDate = React.useMemo<CreatedByDatePoint[]>(() => {
97
+ const map = new Map<string, number>();
98
+ for (const e of data) {
99
+ const key = getCreatedDateKey(e.createdAt);
100
+ map.set(key, (map.get(key) ?? 0) + 1);
101
+ }
102
+ return Array.from(map.entries())
103
+ .map(([date, count]) => ({ date, count }))
104
+ .sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));
105
+ }, [data]);
106
+
107
+ return {
108
+ isLoading,
109
+ isFetching,
110
+ isError,
111
+ refetch,
112
+ totalEscrows,
113
+ totalAmount,
114
+ totalBalance,
115
+ typeSlices,
116
+ amountsByDate,
117
+ createdByDate,
118
+ } as const;
119
+ }
@@ -14,7 +14,8 @@
14
14
  "@creit.tech/stellar-wallets-kit": "^1.8.0",
15
15
  "axios": "^1.7.9",
16
16
  "@tanstack/react-table": "^8.21.3",
17
- "react-day-picker": "^9.5.0"
17
+ "react-day-picker": "^9.5.0",
18
+ "recharts": "^2.13.3"
18
19
  },
19
20
  "devDependencies": {
20
21
  "postcss": "^8",