@burdenoff/microfe-subscriberbot 2026.827.1 → 2026.829.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/dist/subscriberbot/admin/AdminDashboardPage.d.ts.map +1 -1
- package/dist/subscriberbot/admin/AdminDashboardPage.js +80 -83
- package/dist/subscriberbot/admin/AdminDashboardPage.js.map +1 -1
- package/dist/subscriberbot/hooks/index.d.ts +1 -0
- package/dist/subscriberbot/hooks/index.d.ts.map +1 -1
- package/dist/subscriberbot/hooks/index.js +1 -0
- package/dist/subscriberbot/hooks/useTr.d.ts +17 -0
- package/dist/subscriberbot/hooks/useTr.d.ts.map +1 -0
- package/dist/subscriberbot/hooks/useTr.js +14 -0
- package/dist/subscriberbot/hooks/useTr.js.map +1 -0
- package/dist/subscriberbot/pages/AssetVaultPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/AssetVaultPage.js +111 -109
- package/dist/subscriberbot/pages/AssetVaultPage.js.map +1 -1
- package/dist/subscriberbot/pages/DashboardPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/DashboardPage.js +113 -105
- package/dist/subscriberbot/pages/DashboardPage.js.map +1 -1
- package/dist/subscriberbot/pages/InboxPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/InboxPage.js +404 -369
- package/dist/subscriberbot/pages/InboxPage.js.map +1 -1
- package/dist/subscriberbot/pages/InsightsPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/InsightsPage.js +94 -93
- package/dist/subscriberbot/pages/InsightsPage.js.map +1 -1
- package/dist/subscriberbot/pages/InvoicesPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/InvoicesPage.js +360 -366
- package/dist/subscriberbot/pages/InvoicesPage.js.map +1 -1
- package/dist/subscriberbot/pages/WorkflowsPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/WorkflowsPage.js +209 -193
- package/dist/subscriberbot/pages/WorkflowsPage.js.map +1 -1
- package/dist/subscriberbot/pages/agents/AgentDetailPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/agents/AgentDetailPage.js +268 -236
- package/dist/subscriberbot/pages/agents/AgentDetailPage.js.map +1 -1
- package/dist/subscriberbot/pages/agents/AgentNewPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/agents/AgentNewPage.js +96 -76
- package/dist/subscriberbot/pages/agents/AgentNewPage.js.map +1 -1
- package/dist/subscriberbot/pages/agents/AgentsListPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/agents/AgentsListPage.js +145 -161
- package/dist/subscriberbot/pages/agents/AgentsListPage.js.map +1 -1
- package/dist/subscriberbot/pages/providers/ProviderDetailPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/providers/ProviderDetailPage.js +514 -441
- package/dist/subscriberbot/pages/providers/ProviderDetailPage.js.map +1 -1
- package/dist/subscriberbot/pages/providers/ProviderNewPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/providers/ProviderNewPage.js +167 -129
- package/dist/subscriberbot/pages/providers/ProviderNewPage.js.map +1 -1
- package/dist/subscriberbot/pages/providers/ProvidersListPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/providers/ProvidersListPage.js +171 -218
- package/dist/subscriberbot/pages/providers/ProvidersListPage.js.map +1 -1
- package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js +340 -279
- package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js.map +1 -1
- package/dist/subscriberbot/pages/relationships/RelationshipNewPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/relationships/RelationshipNewPage.js +185 -166
- package/dist/subscriberbot/pages/relationships/RelationshipNewPage.js.map +1 -1
- package/dist/subscriberbot/pages/relationships/RelationshipsListPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/relationships/RelationshipsListPage.js +153 -182
- package/dist/subscriberbot/pages/relationships/RelationshipsListPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -6,21 +6,22 @@ import { EmptyState as a } from "../components/EmptyState.js";
|
|
|
6
6
|
import { ErrorView as o, LoadingView as s, Section as c } from "../components/StateViews.js";
|
|
7
7
|
import "../components/index.js";
|
|
8
8
|
import { useAllAssets as l, useFileDownloadUrl as u, useRelationships as d } from "../hooks/useSubscriberBotApi.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
9
|
+
import { useTr as f } from "../hooks/useTr.js";
|
|
10
|
+
import { useMemo as p, useState as m } from "react";
|
|
11
|
+
import { AlarmClock as h, Award as g, Download as _, File as v, FileKey as y, FileText as b, Globe as x, Receipt as S, Search as C, ShieldCheck as w, Vault as T } from "lucide-react";
|
|
12
|
+
import { jsx as E, jsxs as D } from "react/jsx-runtime";
|
|
13
|
+
import { GlassCard as O, PagePurpose as k } from "@burdenoff/fe-libs/ui";
|
|
13
14
|
//#region src/subscriberbot/pages/AssetVaultPage.tsx
|
|
14
|
-
var
|
|
15
|
-
CONTRACT:
|
|
16
|
-
LICENSE_KEY:
|
|
17
|
-
CREDENTIAL:
|
|
18
|
-
CERTIFICATE:
|
|
19
|
-
INVOICE:
|
|
20
|
-
RECEIPT:
|
|
21
|
-
WARRANTY:
|
|
22
|
-
DOMAIN:
|
|
23
|
-
},
|
|
15
|
+
var A = {
|
|
16
|
+
CONTRACT: b,
|
|
17
|
+
LICENSE_KEY: y,
|
|
18
|
+
CREDENTIAL: y,
|
|
19
|
+
CERTIFICATE: g,
|
|
20
|
+
INVOICE: S,
|
|
21
|
+
RECEIPT: S,
|
|
22
|
+
WARRANTY: w,
|
|
23
|
+
DOMAIN: x
|
|
24
|
+
}, j = [
|
|
24
25
|
"CONTRACT",
|
|
25
26
|
"LICENSE_KEY",
|
|
26
27
|
"CREDENTIAL",
|
|
@@ -29,197 +30,198 @@ var k = {
|
|
|
29
30
|
"RECEIPT",
|
|
30
31
|
"WARRANTY",
|
|
31
32
|
"DOMAIN"
|
|
32
|
-
],
|
|
33
|
+
], M = (e) => {
|
|
33
34
|
if (!e) return !1;
|
|
34
35
|
let t = new Date(e).getTime();
|
|
35
36
|
if (Number.isNaN(t)) return !1;
|
|
36
37
|
let n = (t - Date.now()) / 864e5;
|
|
37
38
|
return n >= 0 && n <= 30;
|
|
38
|
-
},
|
|
39
|
+
}, N = (e) => {
|
|
39
40
|
if (!e) return Infinity;
|
|
40
41
|
let t = new Date(e).getTime();
|
|
41
42
|
return Number.isNaN(t) ? Infinity : t;
|
|
42
43
|
};
|
|
43
|
-
function
|
|
44
|
-
let a = u(), [
|
|
45
|
-
return /* @__PURE__ */
|
|
44
|
+
function P({ asset: t, subscription: n }) {
|
|
45
|
+
let a = f(), o = u(), [s, c] = m(!1), l = A[t.type] ?? v, d = M(t.expiresAt);
|
|
46
|
+
return /* @__PURE__ */ D(O, {
|
|
46
47
|
treatment: "metric",
|
|
47
48
|
className: "flex items-start gap-3 p-4",
|
|
48
49
|
children: [
|
|
49
|
-
/* @__PURE__ */
|
|
50
|
+
/* @__PURE__ */ E("span", {
|
|
50
51
|
className: "grid h-10 w-10 shrink-0 place-items-center rounded-lg bg-accent-wash text-text-link",
|
|
51
|
-
children: /* @__PURE__ */
|
|
52
|
+
children: /* @__PURE__ */ E(l, { className: "h-5 w-5" })
|
|
52
53
|
}),
|
|
53
|
-
/* @__PURE__ */
|
|
54
|
+
/* @__PURE__ */ D("div", {
|
|
54
55
|
className: "min-w-0 flex-1",
|
|
55
56
|
children: [
|
|
56
|
-
/* @__PURE__ */
|
|
57
|
+
/* @__PURE__ */ E("p", {
|
|
57
58
|
className: "truncate font-medium text-text-primary",
|
|
58
59
|
children: t.name
|
|
59
60
|
}),
|
|
60
|
-
/* @__PURE__ */
|
|
61
|
+
/* @__PURE__ */ E("p", {
|
|
61
62
|
className: "truncate text-xs text-text-secondary",
|
|
62
63
|
children: n
|
|
63
64
|
}),
|
|
64
|
-
/* @__PURE__ */
|
|
65
|
+
/* @__PURE__ */ D("div", {
|
|
65
66
|
className: "mt-1 flex flex-wrap items-center gap-2 text-xs",
|
|
66
|
-
children: [/* @__PURE__ */
|
|
67
|
+
children: [/* @__PURE__ */ E("span", {
|
|
67
68
|
className: "rounded bg-bg-sunken px-1.5 py-0.5 text-text-secondary",
|
|
68
69
|
children: i(t.type)
|
|
69
70
|
}), t.expiresAt && /* @__PURE__ */ E("span", {
|
|
70
|
-
className: e(
|
|
71
|
-
children:
|
|
71
|
+
className: e(d ? "text-status-warning-text" : "text-text-secondary"),
|
|
72
|
+
children: a("assetVault.expires", "Expires {{date}}", { date: r(t.expiresAt) })
|
|
72
73
|
})]
|
|
73
74
|
})
|
|
74
75
|
]
|
|
75
76
|
}),
|
|
76
|
-
t.file?.id && /* @__PURE__ */
|
|
77
|
+
t.file?.id && /* @__PURE__ */ E("button", {
|
|
77
78
|
type: "button",
|
|
78
79
|
onClick: async () => {
|
|
79
|
-
if (!(!t.file?.id ||
|
|
80
|
-
|
|
80
|
+
if (!(!t.file?.id || s)) {
|
|
81
|
+
c(!0);
|
|
81
82
|
try {
|
|
82
|
-
let e = await
|
|
83
|
+
let e = await o(t.file.id);
|
|
83
84
|
window.open(e, "_blank", "noopener,noreferrer");
|
|
84
85
|
} finally {
|
|
85
|
-
|
|
86
|
+
c(!1);
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
},
|
|
89
|
-
disabled:
|
|
90
|
-
"aria-label":
|
|
90
|
+
disabled: s,
|
|
91
|
+
"aria-label": a("assetVault.downloadAriaLabel", "Download {{name}}", { name: t.name }),
|
|
91
92
|
className: "shrink-0 rounded-lg border border-border-subtle p-2 text-text-secondary hover:text-text-primary disabled:opacity-50",
|
|
92
|
-
children: /* @__PURE__ */
|
|
93
|
+
children: /* @__PURE__ */ E(_, { className: "h-4 w-4" })
|
|
93
94
|
})
|
|
94
95
|
]
|
|
95
96
|
});
|
|
96
97
|
}
|
|
97
|
-
var
|
|
98
|
-
let u = l(),
|
|
98
|
+
var F = () => {
|
|
99
|
+
let u = l(), g = d({ pagination: { limit: 100 } }), _ = f(), [y, x] = m(""), [S, A] = m("ALL"), F = p(() => {
|
|
99
100
|
let e = /* @__PURE__ */ new Map();
|
|
100
|
-
for (let t of
|
|
101
|
+
for (let t of g.data?.items ?? []) e.set(t.id, t.label);
|
|
101
102
|
return e;
|
|
102
|
-
}, [
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
]),
|
|
103
|
+
}, [g.data]), I = p(() => u.data ?? [], [u.data]), L = p(() => I.filter((e) => (S === "ALL" || e.type === S) && (!y || e.name.toLowerCase().includes(y.toLowerCase()))), [
|
|
104
|
+
I,
|
|
105
|
+
S,
|
|
106
|
+
y
|
|
107
|
+
]), R = p(() => {
|
|
107
108
|
let e = /* @__PURE__ */ new Map();
|
|
108
|
-
for (let t of
|
|
109
|
-
return
|
|
110
|
-
}, [
|
|
111
|
-
return u.isLoading ? /* @__PURE__ */
|
|
112
|
-
message: "Could not load the asset vault.",
|
|
109
|
+
for (let t of L) e.set(t.type, [...e.get(t.type) ?? [], t]);
|
|
110
|
+
return j.filter((t) => e.has(t)).map((t) => [t, e.get(t)]);
|
|
111
|
+
}, [L]), z = p(() => I.filter((e) => M(e.expiresAt)).sort((e, t) => N(e.expiresAt) - N(t.expiresAt)), [I]);
|
|
112
|
+
return u.isLoading ? /* @__PURE__ */ E(s, { label: _("assetVault.loading", "Loading your asset vault…") }) : u.isError ? /* @__PURE__ */ E(o, {
|
|
113
|
+
message: _("assetVault.error", "Could not load the asset vault."),
|
|
113
114
|
onRetry: () => void u.refetch()
|
|
114
|
-
}) : /* @__PURE__ */
|
|
115
|
-
title: "Asset vault",
|
|
116
|
-
description: "Every contract, receipt, license key and document attached to your subscriptions — in one secure place.",
|
|
115
|
+
}) : /* @__PURE__ */ D(t, {
|
|
116
|
+
title: _("assetVault.title", "Asset vault"),
|
|
117
|
+
description: _("assetVault.description", "Every contract, receipt, license key and document attached to your subscriptions — in one secure place."),
|
|
117
118
|
children: [
|
|
118
|
-
/* @__PURE__ */
|
|
119
|
+
/* @__PURE__ */ E(k, {
|
|
119
120
|
className: "mb-2",
|
|
120
|
-
children: "Your subscription documents, together — contracts, receipts, license keys and more. Assets are stored in the shared files service and linked to the subscription they belong to. Attach new documents from any relationship’s detail page."
|
|
121
|
+
children: _("assetVault.purpose", "Your subscription documents, together — contracts, receipts, license keys and more. Assets are stored in the shared files service and linked to the subscription they belong to. Attach new documents from any relationship’s detail page.")
|
|
121
122
|
}),
|
|
122
|
-
/* @__PURE__ */
|
|
123
|
+
/* @__PURE__ */ D("div", {
|
|
123
124
|
className: "grid grid-cols-1 gap-4 sm:grid-cols-3",
|
|
124
125
|
children: [
|
|
125
|
-
/* @__PURE__ */
|
|
126
|
-
icon: /* @__PURE__ */
|
|
127
|
-
value: P.length,
|
|
128
|
-
label: "Documents stored"
|
|
129
|
-
}),
|
|
130
|
-
/* @__PURE__ */ T(n, {
|
|
131
|
-
icon: /* @__PURE__ */ T(y, { className: "h-5 w-5" }),
|
|
126
|
+
/* @__PURE__ */ E(n, {
|
|
127
|
+
icon: /* @__PURE__ */ E(v, { className: "h-5 w-5" }),
|
|
132
128
|
value: I.length,
|
|
133
|
-
label: "
|
|
129
|
+
label: _("assetVault.metrics.documentsStored", "Documents stored")
|
|
130
|
+
}),
|
|
131
|
+
/* @__PURE__ */ E(n, {
|
|
132
|
+
icon: /* @__PURE__ */ E(b, { className: "h-5 w-5" }),
|
|
133
|
+
value: R.length,
|
|
134
|
+
label: _("assetVault.metrics.documentTypes", "Document types")
|
|
134
135
|
}),
|
|
135
|
-
/* @__PURE__ */
|
|
136
|
-
icon: /* @__PURE__ */
|
|
137
|
-
value:
|
|
138
|
-
label: "Expiring within 30 days",
|
|
139
|
-
trend:
|
|
136
|
+
/* @__PURE__ */ E(n, {
|
|
137
|
+
icon: /* @__PURE__ */ E(w, { className: "h-5 w-5" }),
|
|
138
|
+
value: z.length,
|
|
139
|
+
label: _("assetVault.metrics.expiring30Days", "Expiring within 30 days"),
|
|
140
|
+
trend: z.length > 0 ? "down" : void 0
|
|
140
141
|
})
|
|
141
142
|
]
|
|
142
143
|
}),
|
|
143
|
-
/* @__PURE__ */
|
|
144
|
+
/* @__PURE__ */ D(O, {
|
|
144
145
|
treatment: "plain",
|
|
145
146
|
className: "flex flex-col gap-3 p-4 sm:flex-row sm:items-center",
|
|
146
|
-
children: [/* @__PURE__ */
|
|
147
|
+
children: [/* @__PURE__ */ D("div", {
|
|
147
148
|
className: "relative flex-1",
|
|
148
|
-
children: [/* @__PURE__ */
|
|
149
|
-
value:
|
|
150
|
-
onChange: (e) =>
|
|
151
|
-
placeholder: "Search documents…",
|
|
152
|
-
"aria-label": "Search documents",
|
|
149
|
+
children: [/* @__PURE__ */ E(C, { className: "pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" }), /* @__PURE__ */ E("input", {
|
|
150
|
+
value: y,
|
|
151
|
+
onChange: (e) => x(e.target.value),
|
|
152
|
+
placeholder: _("assetVault.searchPlaceholder", "Search documents…"),
|
|
153
|
+
"aria-label": _("assetVault.searchAriaLabel", "Search documents"),
|
|
153
154
|
className: "w-full rounded-lg border border-border-subtle bg-bg-surface py-2 pl-9 pr-3 text-sm text-text-primary placeholder:text-text-secondary"
|
|
154
155
|
})]
|
|
155
|
-
}), /* @__PURE__ */
|
|
156
|
+
}), /* @__PURE__ */ E("div", {
|
|
156
157
|
className: "flex flex-wrap gap-1.5",
|
|
157
|
-
children: ["ALL", ...
|
|
158
|
+
children: ["ALL", ...j].map((t) => /* @__PURE__ */ E("button", {
|
|
158
159
|
type: "button",
|
|
159
|
-
onClick: () =>
|
|
160
|
-
"aria-pressed":
|
|
161
|
-
className: e("rounded-full px-3 py-1 text-xs font-medium transition-colors",
|
|
162
|
-
children: t === "ALL" ? "All" : i(t)
|
|
160
|
+
onClick: () => A(t),
|
|
161
|
+
"aria-pressed": S === t,
|
|
162
|
+
className: e("rounded-full px-3 py-1 text-xs font-medium transition-colors", S === t ? "bg-accent-primary text-accent-foreground" : "bg-bg-sunken text-text-secondary hover:text-text-primary"),
|
|
163
|
+
children: t === "ALL" ? _("assetVault.filterAll", "All") : i(t)
|
|
163
164
|
}, t))
|
|
164
165
|
})]
|
|
165
166
|
}),
|
|
166
|
-
/* @__PURE__ */
|
|
167
|
+
/* @__PURE__ */ D("div", {
|
|
167
168
|
className: "grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_20rem]",
|
|
168
|
-
children: [/* @__PURE__ */
|
|
169
|
+
children: [/* @__PURE__ */ E("div", {
|
|
169
170
|
className: "min-w-0",
|
|
170
|
-
children:
|
|
171
|
-
icon: /* @__PURE__ */ T
|
|
172
|
-
title:
|
|
173
|
-
description:
|
|
174
|
-
}) :
|
|
171
|
+
children: L.length === 0 ? /* @__PURE__ */ E(a, {
|
|
172
|
+
icon: /* @__PURE__ */ E(T, { className: "h-8 w-8" }),
|
|
173
|
+
title: I.length === 0 ? _("assetVault.empty.noDocumentsTitle", "No documents yet") : _("assetVault.empty.noMatchesTitle", "No matching documents"),
|
|
174
|
+
description: I.length === 0 ? _("assetVault.empty.noDocumentsDescription", "Attach a contract, receipt or license key from any subscription’s detail page and it will appear here.") : _("assetVault.empty.noMatchesDescription", "Try a different search or type filter.")
|
|
175
|
+
}) : R.map(([e, t]) => /* @__PURE__ */ E(c, {
|
|
175
176
|
title: `${i(e)} · ${t.length}`,
|
|
176
|
-
children: /* @__PURE__ */
|
|
177
|
+
children: /* @__PURE__ */ E("div", {
|
|
177
178
|
className: "grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3",
|
|
178
|
-
children: t.map((e) => /* @__PURE__ */
|
|
179
|
+
children: t.map((e) => /* @__PURE__ */ E(P, {
|
|
179
180
|
asset: e,
|
|
180
|
-
subscription:
|
|
181
|
+
subscription: F.get(e.relationshipId) ?? "—"
|
|
181
182
|
}, e.id))
|
|
182
183
|
})
|
|
183
184
|
}, e))
|
|
184
|
-
}),
|
|
185
|
+
}), I.length > 0 && /* @__PURE__ */ E("aside", {
|
|
185
186
|
className: "lg:sticky lg:top-4 lg:self-start",
|
|
186
|
-
children: /* @__PURE__ */
|
|
187
|
+
children: /* @__PURE__ */ D(O, {
|
|
187
188
|
treatment: "insight",
|
|
188
189
|
className: "p-5",
|
|
189
190
|
children: [
|
|
190
|
-
/* @__PURE__ */
|
|
191
|
+
/* @__PURE__ */ D("h3", {
|
|
191
192
|
className: "flex items-center gap-2 text-sm font-semibold text-text-primary",
|
|
192
|
-
children: [/* @__PURE__ */
|
|
193
|
+
children: [/* @__PURE__ */ E(h, {
|
|
193
194
|
className: "h-4 w-4 text-status-warning-text",
|
|
194
195
|
"aria-hidden": !0
|
|
195
|
-
}), "Expiring soon"]
|
|
196
|
+
}), _("assetVault.expiringSoon.title", "Expiring soon")]
|
|
196
197
|
}),
|
|
197
|
-
/* @__PURE__ */
|
|
198
|
+
/* @__PURE__ */ E("p", {
|
|
198
199
|
className: "mt-0.5 text-xs text-text-secondary",
|
|
199
|
-
children: "Documents lapsing in the next 30 days"
|
|
200
|
+
children: _("assetVault.expiringSoon.description", "Documents lapsing in the next 30 days")
|
|
200
201
|
}),
|
|
201
|
-
|
|
202
|
+
z.length === 0 ? /* @__PURE__ */ E("p", {
|
|
202
203
|
className: "mt-4 text-sm text-text-secondary",
|
|
203
|
-
children: "Nothing in the vault expires in the next 30 days."
|
|
204
|
-
}) : /* @__PURE__ */
|
|
204
|
+
children: _("assetVault.expiringSoon.nothing", "Nothing in the vault expires in the next 30 days.")
|
|
205
|
+
}) : /* @__PURE__ */ E("ul", {
|
|
205
206
|
className: "mt-4 space-y-2.5",
|
|
206
|
-
children:
|
|
207
|
+
children: z.map((e) => /* @__PURE__ */ D("li", {
|
|
207
208
|
className: "rounded-lg border border-status-warning-border bg-status-warning-bg-subtle p-3",
|
|
208
209
|
children: [
|
|
209
|
-
/* @__PURE__ */
|
|
210
|
+
/* @__PURE__ */ E("p", {
|
|
210
211
|
className: "truncate text-sm font-medium text-status-warning-text",
|
|
211
212
|
children: e.name
|
|
212
213
|
}),
|
|
213
|
-
/* @__PURE__ */
|
|
214
|
+
/* @__PURE__ */ E("p", {
|
|
214
215
|
className: "truncate text-xs text-text-secondary",
|
|
215
|
-
children:
|
|
216
|
+
children: F.get(e.relationshipId) ?? "—"
|
|
216
217
|
}),
|
|
217
|
-
/* @__PURE__ */
|
|
218
|
+
/* @__PURE__ */ D("p", {
|
|
218
219
|
className: "mt-1 text-xs text-text-secondary",
|
|
219
220
|
children: [
|
|
220
221
|
i(e.type),
|
|
221
|
-
" ·
|
|
222
|
-
|
|
222
|
+
" ·",
|
|
223
|
+
" ",
|
|
224
|
+
_("assetVault.expires", "Expires {{date}}", { date: r(e.expiresAt) })
|
|
223
225
|
]
|
|
224
226
|
})
|
|
225
227
|
]
|
|
@@ -233,6 +235,6 @@ var P = () => {
|
|
|
233
235
|
});
|
|
234
236
|
};
|
|
235
237
|
//#endregion
|
|
236
|
-
export {
|
|
238
|
+
export { F as AssetVaultPage };
|
|
237
239
|
|
|
238
240
|
//# sourceMappingURL=AssetVaultPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssetVaultPage.js","names":[],"sources":["../../../src/subscriberbot/pages/AssetVaultPage.tsx"],"sourcesContent":["/**\n * Digital Asset Vault — every document attached to any subscription in the\n * workspace (contracts, receipts, license keys, warranties). Real data only:\n * `useAllAssets` (workspace-wide) joined to `useRelationships` for the owning\n * subscription label; downloads resolve a presigned URL via files-svc. The\n * \"Expiring soon\" rail lists the real assets whose `expiresAt` falls inside the\n * next 30 days — the same set the headline metric counts.\n * @module subscriberbot/pages\n */\nimport { useMemo, useState, type FC } from 'react';\nimport {\n FileText,\n FileKey,\n Receipt,\n ShieldCheck,\n File as FileIcon,\n Globe,\n Award,\n Search,\n Download,\n Vault,\n AlarmClock,\n} from 'lucide-react';\nimport { GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n MetricsCard,\n EmptyState,\n LoadingView,\n ErrorView,\n Section,\n} from '../components';\nimport { useAllAssets, useRelationships, useFileDownloadUrl } from '../hooks/useSubscriberBotApi';\nimport { formatDate, formatEnumLabel } from '../utils/formatters';\nimport type { SubAsset, SubAssetType } from '../types';\nimport { cn } from '../utils/cn';\n\nconst TYPE_ICON: Record<SubAssetType, typeof FileText> = {\n CONTRACT: FileText,\n LICENSE_KEY: FileKey,\n CREDENTIAL: FileKey,\n CERTIFICATE: Award,\n INVOICE: Receipt,\n RECEIPT: Receipt,\n WARRANTY: ShieldCheck,\n DOMAIN: Globe,\n};\n\nconst TYPE_ORDER: SubAssetType[] = [\n 'CONTRACT',\n 'LICENSE_KEY',\n 'CREDENTIAL',\n 'CERTIFICATE',\n 'INVOICE',\n 'RECEIPT',\n 'WARRANTY',\n 'DOMAIN',\n];\n\nconst isExpiringSoon = (expiresAt: string | null): boolean => {\n if (!expiresAt) return false;\n const t = new Date(expiresAt).getTime();\n if (Number.isNaN(t)) return false;\n const days = (t - Date.now()) / 86_400_000;\n return days >= 0 && days <= 30;\n};\n\n/** Sort key for expiry — undated/unparseable assets sink to the bottom. */\nconst expiryTime = (expiresAt: string | null): number => {\n if (!expiresAt) return Number.POSITIVE_INFINITY;\n const t = new Date(expiresAt).getTime();\n return Number.isNaN(t) ? Number.POSITIVE_INFINITY : t;\n};\n\nfunction AssetCard({ asset, subscription }: { asset: SubAsset; subscription: string }) {\n const getDownloadUrl = useFileDownloadUrl();\n const [busy, setBusy] = useState(false);\n const Icon = TYPE_ICON[asset.type] ?? FileIcon;\n const soon = isExpiringSoon(asset.expiresAt);\n\n const download = async () => {\n if (!asset.file?.id || busy) return;\n setBusy(true);\n try {\n const url = await getDownloadUrl(asset.file.id);\n window.open(url, '_blank', 'noopener,noreferrer');\n } finally {\n setBusy(false);\n }\n };\n\n return (\n <GlassCard treatment=\"metric\" className=\"flex items-start gap-3 p-4\">\n <span className=\"grid h-10 w-10 shrink-0 place-items-center rounded-lg bg-accent-wash text-text-link\">\n <Icon className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate font-medium text-text-primary\">{asset.name}</p>\n <p className=\"truncate text-xs text-text-secondary\">{subscription}</p>\n <div className=\"mt-1 flex flex-wrap items-center gap-2 text-xs\">\n <span className=\"rounded bg-bg-sunken px-1.5 py-0.5 text-text-secondary\">\n {formatEnumLabel(asset.type)}\n </span>\n {asset.expiresAt && (\n <span className={cn(soon ? 'text-status-warning-text' : 'text-text-secondary')}>\n Expires {formatDate(asset.expiresAt)}\n </span>\n )}\n </div>\n </div>\n {asset.file?.id && (\n <button\n type=\"button\"\n onClick={download}\n disabled={busy}\n aria-label={`Download ${asset.name}`}\n className=\"shrink-0 rounded-lg border border-border-subtle p-2 text-text-secondary hover:text-text-primary disabled:opacity-50\"\n >\n <Download className=\"h-4 w-4\" />\n </button>\n )}\n </GlassCard>\n );\n}\n\nexport const AssetVaultPage: FC = () => {\n const assetsQuery = useAllAssets();\n const relQuery = useRelationships({ pagination: { limit: 100 } });\n const [search, setSearch] = useState('');\n const [typeFilter, setTypeFilter] = useState<SubAssetType | 'ALL'>('ALL');\n\n const relLabels = useMemo(() => {\n const map = new Map<string, string>();\n for (const r of relQuery.data?.items ?? []) map.set(r.id, r.label);\n return map;\n }, [relQuery.data]);\n\n const assets = useMemo(() => assetsQuery.data ?? [], [assetsQuery.data]);\n const filtered = useMemo(\n () =>\n assets.filter(\n (a) =>\n (typeFilter === 'ALL' || a.type === typeFilter) &&\n (!search || a.name.toLowerCase().includes(search.toLowerCase()))\n ),\n [assets, typeFilter, search]\n );\n\n const grouped = useMemo(() => {\n const g = new Map<SubAssetType, SubAsset[]>();\n for (const a of filtered) g.set(a.type, [...(g.get(a.type) ?? []), a]);\n return TYPE_ORDER.filter((t) => g.has(t)).map((t) => [t, g.get(t)!] as const);\n }, [filtered]);\n\n const expiringSoon = useMemo(\n () =>\n assets\n .filter((a) => isExpiringSoon(a.expiresAt))\n .sort((a, b) => expiryTime(a.expiresAt) - expiryTime(b.expiresAt)),\n [assets]\n );\n\n if (assetsQuery.isLoading) return <LoadingView label=\"Loading your asset vault…\" />;\n if (assetsQuery.isError)\n return (\n <ErrorView\n message=\"Could not load the asset vault.\"\n onRetry={() => void assetsQuery.refetch()}\n />\n );\n\n return (\n <PageLayout\n title=\"Asset vault\"\n description=\"Every contract, receipt, license key and document attached to your subscriptions — in one secure place.\"\n >\n <PagePurpose className=\"mb-2\">\n Your subscription documents, together — contracts, receipts, license keys and more. Assets\n are stored in the shared files service and linked to the subscription they belong to. Attach\n new documents from any relationship’s detail page.\n </PagePurpose>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <MetricsCard\n icon={<FileIcon className=\"h-5 w-5\" />}\n value={assets.length}\n label=\"Documents stored\"\n />\n <MetricsCard\n icon={<FileText className=\"h-5 w-5\" />}\n value={grouped.length}\n label=\"Document types\"\n />\n <MetricsCard\n icon={<ShieldCheck className=\"h-5 w-5\" />}\n value={expiringSoon.length}\n label=\"Expiring within 30 days\"\n trend={expiringSoon.length > 0 ? 'down' : undefined}\n />\n </div>\n\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-3 p-4 sm:flex-row sm:items-center\">\n <div className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n placeholder=\"Search documents…\"\n aria-label=\"Search documents\"\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2 pl-9 pr-3 text-sm text-text-primary placeholder:text-text-secondary\"\n />\n </div>\n <div className=\"flex flex-wrap gap-1.5\">\n {(['ALL', ...TYPE_ORDER] as const).map((t) => (\n <button\n key={t}\n type=\"button\"\n onClick={() => setTypeFilter(t)}\n aria-pressed={typeFilter === t}\n className={cn(\n 'rounded-full px-3 py-1 text-xs font-medium transition-colors',\n typeFilter === t\n ? 'bg-accent-primary text-accent-foreground'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {t === 'ALL' ? 'All' : formatEnumLabel(t)}\n </button>\n ))}\n </div>\n </GlassCard>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_20rem]\">\n <div className=\"min-w-0\">\n {filtered.length === 0 ? (\n <EmptyState\n icon={<Vault className=\"h-8 w-8\" />}\n title={assets.length === 0 ? 'No documents yet' : 'No matching documents'}\n description={\n assets.length === 0\n ? 'Attach a contract, receipt or license key from any subscription’s detail page and it will appear here.'\n : 'Try a different search or type filter.'\n }\n />\n ) : (\n grouped.map(([type, items]) => (\n <Section key={type} title={`${formatEnumLabel(type)} · ${items.length}`}>\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3\">\n {items.map((a) => (\n <AssetCard\n key={a.id}\n asset={a}\n subscription={relLabels.get(a.relationshipId) ?? '—'}\n />\n ))}\n </div>\n </Section>\n ))\n )}\n </div>\n\n {assets.length > 0 && (\n <aside className=\"lg:sticky lg:top-4 lg:self-start\">\n <GlassCard treatment=\"insight\" className=\"p-5\">\n <h3 className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <AlarmClock className=\"h-4 w-4 text-status-warning-text\" aria-hidden />\n Expiring soon\n </h3>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n Documents lapsing in the next 30 days\n </p>\n {expiringSoon.length === 0 ? (\n <p className=\"mt-4 text-sm text-text-secondary\">\n Nothing in the vault expires in the next 30 days.\n </p>\n ) : (\n <ul className=\"mt-4 space-y-2.5\">\n {expiringSoon.map((a) => (\n <li\n key={a.id}\n className=\"rounded-lg border border-status-warning-border bg-status-warning-bg-subtle p-3\"\n >\n <p className=\"truncate text-sm font-medium text-status-warning-text\">\n {a.name}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {relLabels.get(a.relationshipId) ?? '—'}\n </p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {formatEnumLabel(a.type)} · Expires {formatDate(a.expiresAt)}\n </p>\n </li>\n ))}\n </ul>\n )}\n </GlassCard>\n </aside>\n )}\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AssetVaultPage.js","names":[],"sources":["../../../src/subscriberbot/pages/AssetVaultPage.tsx"],"sourcesContent":["/**\n * Digital Asset Vault — every document attached to any subscription in the\n * workspace (contracts, receipts, license keys, warranties). Real data only:\n * `useAllAssets` (workspace-wide) joined to `useRelationships` for the owning\n * subscription label; downloads resolve a presigned URL via files-svc. The\n * \"Expiring soon\" rail lists the real assets whose `expiresAt` falls inside the\n * next 30 days — the same set the headline metric counts.\n * @module subscriberbot/pages\n */\nimport { useMemo, useState, type FC } from 'react';\nimport {\n FileText,\n FileKey,\n Receipt,\n ShieldCheck,\n File as FileIcon,\n Globe,\n Award,\n Search,\n Download,\n Vault,\n AlarmClock,\n} from 'lucide-react';\nimport { GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n MetricsCard,\n EmptyState,\n LoadingView,\n ErrorView,\n Section,\n} from '../components';\nimport { useAllAssets, useRelationships, useFileDownloadUrl } from '../hooks/useSubscriberBotApi';\nimport { useTr } from '../hooks/useTr';\nimport { formatDate, formatEnumLabel } from '../utils/formatters';\nimport type { SubAsset, SubAssetType } from '../types';\nimport { cn } from '../utils/cn';\n\nconst TYPE_ICON: Record<SubAssetType, typeof FileText> = {\n CONTRACT: FileText,\n LICENSE_KEY: FileKey,\n CREDENTIAL: FileKey,\n CERTIFICATE: Award,\n INVOICE: Receipt,\n RECEIPT: Receipt,\n WARRANTY: ShieldCheck,\n DOMAIN: Globe,\n};\n\nconst TYPE_ORDER: SubAssetType[] = [\n 'CONTRACT',\n 'LICENSE_KEY',\n 'CREDENTIAL',\n 'CERTIFICATE',\n 'INVOICE',\n 'RECEIPT',\n 'WARRANTY',\n 'DOMAIN',\n];\n\nconst isExpiringSoon = (expiresAt: string | null): boolean => {\n if (!expiresAt) return false;\n const t = new Date(expiresAt).getTime();\n if (Number.isNaN(t)) return false;\n const days = (t - Date.now()) / 86_400_000;\n return days >= 0 && days <= 30;\n};\n\n/** Sort key for expiry — undated/unparseable assets sink to the bottom. */\nconst expiryTime = (expiresAt: string | null): number => {\n if (!expiresAt) return Number.POSITIVE_INFINITY;\n const t = new Date(expiresAt).getTime();\n return Number.isNaN(t) ? Number.POSITIVE_INFINITY : t;\n};\n\nfunction AssetCard({ asset, subscription }: { asset: SubAsset; subscription: string }) {\n const tr = useTr();\n const getDownloadUrl = useFileDownloadUrl();\n const [busy, setBusy] = useState(false);\n const Icon = TYPE_ICON[asset.type] ?? FileIcon;\n const soon = isExpiringSoon(asset.expiresAt);\n\n const download = async () => {\n if (!asset.file?.id || busy) return;\n setBusy(true);\n try {\n const url = await getDownloadUrl(asset.file.id);\n window.open(url, '_blank', 'noopener,noreferrer');\n } finally {\n setBusy(false);\n }\n };\n\n return (\n <GlassCard treatment=\"metric\" className=\"flex items-start gap-3 p-4\">\n <span className=\"grid h-10 w-10 shrink-0 place-items-center rounded-lg bg-accent-wash text-text-link\">\n <Icon className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate font-medium text-text-primary\">{asset.name}</p>\n <p className=\"truncate text-xs text-text-secondary\">{subscription}</p>\n <div className=\"mt-1 flex flex-wrap items-center gap-2 text-xs\">\n <span className=\"rounded bg-bg-sunken px-1.5 py-0.5 text-text-secondary\">\n {formatEnumLabel(asset.type)}\n </span>\n {asset.expiresAt && (\n <span className={cn(soon ? 'text-status-warning-text' : 'text-text-secondary')}>\n {tr('assetVault.expires', 'Expires {{date}}', { date: formatDate(asset.expiresAt) })}\n </span>\n )}\n </div>\n </div>\n {asset.file?.id && (\n <button\n type=\"button\"\n onClick={download}\n disabled={busy}\n aria-label={tr('assetVault.downloadAriaLabel', 'Download {{name}}', { name: asset.name })}\n className=\"shrink-0 rounded-lg border border-border-subtle p-2 text-text-secondary hover:text-text-primary disabled:opacity-50\"\n >\n <Download className=\"h-4 w-4\" />\n </button>\n )}\n </GlassCard>\n );\n}\n\nexport const AssetVaultPage: FC = () => {\n const assetsQuery = useAllAssets();\n const relQuery = useRelationships({ pagination: { limit: 100 } });\n const tr = useTr();\n const [search, setSearch] = useState('');\n const [typeFilter, setTypeFilter] = useState<SubAssetType | 'ALL'>('ALL');\n\n const relLabels = useMemo(() => {\n const map = new Map<string, string>();\n for (const r of relQuery.data?.items ?? []) map.set(r.id, r.label);\n return map;\n }, [relQuery.data]);\n\n const assets = useMemo(() => assetsQuery.data ?? [], [assetsQuery.data]);\n const filtered = useMemo(\n () =>\n assets.filter(\n (a) =>\n (typeFilter === 'ALL' || a.type === typeFilter) &&\n (!search || a.name.toLowerCase().includes(search.toLowerCase()))\n ),\n [assets, typeFilter, search]\n );\n\n const grouped = useMemo(() => {\n const g = new Map<SubAssetType, SubAsset[]>();\n for (const a of filtered) g.set(a.type, [...(g.get(a.type) ?? []), a]);\n return TYPE_ORDER.filter((t) => g.has(t)).map((t) => [t, g.get(t)!] as const);\n }, [filtered]);\n\n const expiringSoon = useMemo(\n () =>\n assets\n .filter((a) => isExpiringSoon(a.expiresAt))\n .sort((a, b) => expiryTime(a.expiresAt) - expiryTime(b.expiresAt)),\n [assets]\n );\n\n if (assetsQuery.isLoading)\n return <LoadingView label={tr('assetVault.loading', 'Loading your asset vault…')} />;\n if (assetsQuery.isError)\n return (\n <ErrorView\n message={tr('assetVault.error', 'Could not load the asset vault.')}\n onRetry={() => void assetsQuery.refetch()}\n />\n );\n\n return (\n <PageLayout\n title={tr('assetVault.title', 'Asset vault')}\n description={tr(\n 'assetVault.description',\n 'Every contract, receipt, license key and document attached to your subscriptions — in one secure place.'\n )}\n >\n <PagePurpose className=\"mb-2\">\n {tr(\n 'assetVault.purpose',\n 'Your subscription documents, together — contracts, receipts, license keys and more. Assets are stored in the shared files service and linked to the subscription they belong to. Attach new documents from any relationship’s detail page.'\n )}\n </PagePurpose>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <MetricsCard\n icon={<FileIcon className=\"h-5 w-5\" />}\n value={assets.length}\n label={tr('assetVault.metrics.documentsStored', 'Documents stored')}\n />\n <MetricsCard\n icon={<FileText className=\"h-5 w-5\" />}\n value={grouped.length}\n label={tr('assetVault.metrics.documentTypes', 'Document types')}\n />\n <MetricsCard\n icon={<ShieldCheck className=\"h-5 w-5\" />}\n value={expiringSoon.length}\n label={tr('assetVault.metrics.expiring30Days', 'Expiring within 30 days')}\n trend={expiringSoon.length > 0 ? 'down' : undefined}\n />\n </div>\n\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-3 p-4 sm:flex-row sm:items-center\">\n <div className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n placeholder={tr('assetVault.searchPlaceholder', 'Search documents…')}\n aria-label={tr('assetVault.searchAriaLabel', 'Search documents')}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2 pl-9 pr-3 text-sm text-text-primary placeholder:text-text-secondary\"\n />\n </div>\n <div className=\"flex flex-wrap gap-1.5\">\n {(['ALL', ...TYPE_ORDER] as const).map((t) => (\n <button\n key={t}\n type=\"button\"\n onClick={() => setTypeFilter(t)}\n aria-pressed={typeFilter === t}\n className={cn(\n 'rounded-full px-3 py-1 text-xs font-medium transition-colors',\n typeFilter === t\n ? 'bg-accent-primary text-accent-foreground'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {t === 'ALL' ? tr('assetVault.filterAll', 'All') : formatEnumLabel(t)}\n </button>\n ))}\n </div>\n </GlassCard>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_20rem]\">\n <div className=\"min-w-0\">\n {filtered.length === 0 ? (\n <EmptyState\n icon={<Vault className=\"h-8 w-8\" />}\n title={\n assets.length === 0\n ? tr('assetVault.empty.noDocumentsTitle', 'No documents yet')\n : tr('assetVault.empty.noMatchesTitle', 'No matching documents')\n }\n description={\n assets.length === 0\n ? tr(\n 'assetVault.empty.noDocumentsDescription',\n 'Attach a contract, receipt or license key from any subscription’s detail page and it will appear here.'\n )\n : tr(\n 'assetVault.empty.noMatchesDescription',\n 'Try a different search or type filter.'\n )\n }\n />\n ) : (\n grouped.map(([type, items]) => (\n <Section key={type} title={`${formatEnumLabel(type)} · ${items.length}`}>\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3\">\n {items.map((a) => (\n <AssetCard\n key={a.id}\n asset={a}\n subscription={relLabels.get(a.relationshipId) ?? '—'}\n />\n ))}\n </div>\n </Section>\n ))\n )}\n </div>\n\n {assets.length > 0 && (\n <aside className=\"lg:sticky lg:top-4 lg:self-start\">\n <GlassCard treatment=\"insight\" className=\"p-5\">\n <h3 className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <AlarmClock className=\"h-4 w-4 text-status-warning-text\" aria-hidden />\n {tr('assetVault.expiringSoon.title', 'Expiring soon')}\n </h3>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {tr('assetVault.expiringSoon.description', 'Documents lapsing in the next 30 days')}\n </p>\n {expiringSoon.length === 0 ? (\n <p className=\"mt-4 text-sm text-text-secondary\">\n {tr(\n 'assetVault.expiringSoon.nothing',\n 'Nothing in the vault expires in the next 30 days.'\n )}\n </p>\n ) : (\n <ul className=\"mt-4 space-y-2.5\">\n {expiringSoon.map((a) => (\n <li\n key={a.id}\n className=\"rounded-lg border border-status-warning-border bg-status-warning-bg-subtle p-3\"\n >\n <p className=\"truncate text-sm font-medium text-status-warning-text\">\n {a.name}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {relLabels.get(a.relationshipId) ?? '—'}\n </p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {formatEnumLabel(a.type)} ·{' '}\n {tr('assetVault.expires', 'Expires {{date}}', {\n date: formatDate(a.expiresAt),\n })}\n </p>\n </li>\n ))}\n </ul>\n )}\n </GlassCard>\n </aside>\n )}\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardPage.d.ts","sourceRoot":"","sources":["../../../src/subscriberbot/pages/DashboardPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;
|
|
1
|
+
{"version":3,"file":"DashboardPage.d.ts","sourceRoot":"","sources":["../../../src/subscriberbot/pages/DashboardPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAoEH,wBAAgB,aAAa,gCAwc5B"}
|