forked from FlowwStar/FlowStar
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdashboard-stats.tsx
More file actions
162 lines (151 loc) · 4.78 KB
/
Copy pathdashboard-stats.tsx
File metadata and controls
162 lines (151 loc) · 4.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
"use client";
import { useNow } from "@/hooks/use-now";
import { usePortfolioValue, formatUsd } from "@/hooks/use-token-price";
import { useShowUsd } from "@/hooks/use-show-usd";
import { getStreamStatus, getWithdrawableAmount } from "@/lib/stream-utils";
import { TokenAmount } from "@/components/ui/token-amount";
import type { StreamData } from "@/types/stream";
interface DashboardStatsProps {
sent: StreamData[];
received: StreamData[];
}
/**
* Aggregates per-token totals. Only ticks every second when there are
* active (streaming) streams; otherwise uses a 60s interval to avoid
* unnecessary re-renders on dashboards with only static streams.
*/
export function DashboardStats({ sent, received }: DashboardStatsProps) {
const nowSec = Math.floor(Date.now() / 1000);
const hasActiveStreams =
sent.some(
(s) =>
!s.cancelled &&
nowSec < Number(s.endTime) &&
nowSec >= Number(s.startTime),
) ||
received.some(
(s) =>
!s.cancelled &&
nowSec < Number(s.endTime) &&
nowSec >= Number(s.startTime),
);
const now = useNow(hasActiveStreams ? 1000 : 60000);
const [showUsd] = useShowUsd();
const {
totalUsd,
loading: priceLoading,
stale,
} = usePortfolioValue([...sent, ...received]);
const activeReceiving = received.filter(
(s) => getStreamStatus(s, now) === "streaming",
).length;
const activeSending = sent.filter(
(s) => getStreamStatus(s, now) === "streaming",
).length;
const withdrawableByToken = new Map<
string,
{ amount: bigint; token: StreamData["token"] }
>();
for (const s of received) {
const amt = getWithdrawableAmount(s, now);
const existing = withdrawableByToken.get(s.token.symbol);
if (existing) existing.amount += amt;
else
withdrawableByToken.set(s.token.symbol, { amount: amt, token: s.token });
}
const topWithdrawable = [...withdrawableByToken.values()].sort((a, b) =>
a.amount > b.amount ? -1 : 1,
)[0];
const usdDisplay = showUsd ? (
priceLoading ? (
<span className="inline-block h-6 w-20 animate-pulse rounded bg-muted" />
) : totalUsd !== null ? (
<span>
{formatUsd(totalUsd)}
{stale && (
<span className="ml-1 text-base" role="img" aria-label="Price may be outdated">
⚠️
</span>
)}
</span>
) : (
<span className="text-muted-foreground">—</span>
)
) : (
<span className="text-muted-foreground">—</span>
);
const stats = [
{
label: "Total streaming value",
value: usdDisplay,
hint:
showUsd && totalUsd !== null
? "locked across all streams"
: "enable USD in settings",
testId: "stat-total-streaming",
},
{
label: "Available to withdraw",
value: topWithdrawable ? (
<TokenAmount
amount={topWithdrawable.amount}
token={topWithdrawable.token}
maxFractionDigits={2}
/>
) : (
<span className="text-muted-foreground">—</span>
),
hint:
withdrawableByToken.size > 1
? `+${withdrawableByToken.size - 1} more token${withdrawableByToken.size > 2 ? "s" : ""}`
: "across received streams",
testId: "stat-available-to-withdraw",
},
{
label: "Receiving",
value: <span>{received.length}</span>,
hint: `${activeReceiving} streaming now`,
testId: "stat-receiving",
},
{
label: "Sending",
value: <span>{sent.length}</span>,
hint: `${activeSending} streaming now`,
testId: "stat-sending",
},
];
return (
<div className="grid gap-4 sm:grid-cols-4">
{stats.map((stat) => (
<div
key={stat.label}
className="rounded-2xl border border-border bg-card p-5"
data-testid={stat.testId}
>
<p className="text-sm text-muted-foreground">{stat.label}</p>
<p className="mt-2 font-mono text-2xl font-semibold tabular-nums">
{stat.value}
</p>
<p className="mt-1 text-xs text-muted-foreground">{stat.hint}</p>
</div>
))}
</div>
);
}
// ─── Skeleton ────────────────────────────────────────────────────────────────
export function DashboardStatsSkeleton() {
return (
<div className="grid gap-4 sm:grid-cols-4">
{[0, 1, 2, 3].map((i) => (
<div
key={i}
className="rounded-2xl border border-border bg-card p-5 animate-pulse"
>
<div className="h-3.5 w-32 rounded bg-muted" />
<div className="mt-2 h-8 w-24 rounded bg-muted" />
<div className="mt-1 h-3 w-28 rounded bg-muted" />
</div>
))}
</div>
);
}