@tollbooth-dpyc/web 1.3.1 → 1.4.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 +65 -4
- package/dist/buildInfo.d.ts +21 -0
- package/dist/buildInfo.js +35 -0
- package/dist/client.js +4 -4
- package/dist/debugLog.d.ts +4 -2
- package/dist/debugLog.js +59 -16
- package/dist/funding.d.ts +73 -0
- package/dist/funding.js +160 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/react/BuildInfoPanel.d.ts +50 -0
- package/dist/react/BuildInfoPanel.js +49 -0
- package/dist/react/CouponsPanel.js +1 -4
- package/dist/react/FundingStatus.d.ts +67 -0
- package/dist/react/FundingStatus.js +120 -0
- package/dist/react/PagedTable.js +1 -4
- package/dist/react/TableFilter.js +1 -4
- package/dist/react/TimezonePicker.d.ts +30 -0
- package/dist/react/TimezonePicker.js +19 -0
- package/dist/react/UsageSummary.d.ts +55 -0
- package/dist/react/UsageSummary.js +53 -0
- package/dist/react/WalletPage.js +1 -4
- package/dist/react/cx.d.ts +2 -0
- package/dist/react/cx.js +5 -0
- package/dist/react/index.d.ts +5 -0
- package/dist/react/index.js +5 -0
- package/dist/react/useTimezone.d.ts +8 -0
- package/dist/react/useTimezone.js +34 -0
- package/dist/timezone.d.ts +82 -0
- package/dist/timezone.js +235 -0
- package/dist/usage.d.ts +34 -0
- package/dist/usage.js +53 -0
- package/package.json +1 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Build & license: what is running and under what terms — the front end's
|
|
4
|
+
* version and commit (the site's, passed in, since only its bundler knows
|
|
5
|
+
* them), the MCP server's version, wheel and deployed commit linked to its
|
|
6
|
+
* repository (from `service_status`), the Tollbooth-DPYC™ links, the licence,
|
|
7
|
+
* and the patent and trademark notice.
|
|
8
|
+
*
|
|
9
|
+
* Mechanics only: every visual choice is the site's, through `classNames`;
|
|
10
|
+
* `intro` carries the site's own words and `children` any rows of its own,
|
|
11
|
+
* drawn after the MCP server section. Pass `status` when the site already
|
|
12
|
+
* holds a `service_status` answer; without it the panel asks once.
|
|
13
|
+
*/
|
|
14
|
+
import { useEffect, useState } from "react";
|
|
15
|
+
import { buildFacts } from "../buildInfo.js";
|
|
16
|
+
import { serviceStatus } from "../standardTools.js";
|
|
17
|
+
import { cx } from "./cx.js";
|
|
18
|
+
const APACHE = { name: "Apache 2.0", href: "https://www.apache.org/licenses/LICENSE-2.0" };
|
|
19
|
+
function bare(href) {
|
|
20
|
+
return href.replace(/^https:\/\//, "").replace(/\/+$/, "");
|
|
21
|
+
}
|
|
22
|
+
function isHttps(href) {
|
|
23
|
+
return !!href && href.startsWith("https://");
|
|
24
|
+
}
|
|
25
|
+
function Section({ c, children }) {
|
|
26
|
+
return _jsx("div", { className: c.section, children: children });
|
|
27
|
+
}
|
|
28
|
+
function Row({ c, label, value, href }) {
|
|
29
|
+
return (_jsxs("div", { className: c.row, children: [_jsx("span", { className: c.label, children: label }), " ", href ? (_jsx("a", { href: href, target: "_blank", rel: "noopener noreferrer", className: cx(c.value, c.link), children: value })) : (_jsx("span", { className: c.value, children: value }))] }));
|
|
30
|
+
}
|
|
31
|
+
export default function BuildInfoPanel({ status, frontend, heading = "Build & license", intro, licence = APACHE, children, classNames: c = {}, }) {
|
|
32
|
+
const [fetched, setFetched] = useState(null);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (status !== undefined)
|
|
35
|
+
return;
|
|
36
|
+
let live = true;
|
|
37
|
+
serviceStatus()
|
|
38
|
+
.then((s) => live && setFetched(s))
|
|
39
|
+
.catch(() => {
|
|
40
|
+
/* the rows read "—" */
|
|
41
|
+
});
|
|
42
|
+
return () => {
|
|
43
|
+
live = false;
|
|
44
|
+
};
|
|
45
|
+
}, [status]);
|
|
46
|
+
const f = buildFacts(status === undefined ? fetched : status);
|
|
47
|
+
const feVersion = [frontend?.version, frontend?.commit].filter(Boolean).join(" · ");
|
|
48
|
+
return (_jsxs("section", { className: c.root, children: [heading && _jsx("div", { className: c.heading, children: heading }), intro && _jsx("div", { className: c.intro, children: intro }), frontend && (_jsxs(_Fragment, { children: [_jsx(Section, { c: c, children: "Frontend" }), _jsx(Row, { c: c, label: "Version", value: feVersion || "—" }), frontend.builtAt && _jsx(Row, { c: c, label: "Built at", value: frontend.builtAt }), isHttps(frontend.source) && _jsx(Row, { c: c, label: "Source", value: bare(frontend.source), href: frontend.source })] })), _jsx(Section, { c: c, children: "MCP server" }), _jsx(Row, { c: c, label: "Version", value: f.version ?? "—" }), _jsx(Row, { c: c, label: "Commit", value: f.commit ?? "—", href: f.commitHref }), _jsx(Row, { c: c, label: "tollbooth-dpyc", value: f.sdkVersion ? `wheel ${f.sdkVersion}` : "—" }), f.repo && _jsx(Row, { c: c, label: "Source", value: f.repo, href: f.repoHref }), children, _jsx(Section, { c: c, children: "Tollbooth-DPYC\u2122" }), _jsx(Row, { c: c, label: "Marketing", value: "tollbooth-dpyc.com", href: "https://tollbooth-dpyc.com" }), _jsx(Row, { c: c, label: "Community", value: "github.com/lonniev/dpyc-community", href: "https://github.com/lonniev/dpyc-community" }), _jsx(Row, { c: c, label: "Wheel source", value: "github.com/lonniev/tollbooth-dpyc", href: "https://github.com/lonniev/tollbooth-dpyc" }), _jsx(Section, { c: c, children: "License" }), _jsx(Row, { c: c, label: licence.name, value: licence.href ? bare(licence.href) : licence.name, href: isHttps(licence.href) ? licence.href : null }), _jsx(Section, { c: c, children: "Patent & trademarks" }), _jsx(Row, { c: c, label: "Patent", value: "Patent Pending \u2014 US Provisional 64/045,999" }), _jsx(Row, { c: c, label: "Filing", value: "dpyc-community/docs/patent", href: "https://github.com/lonniev/dpyc-community/tree/main/docs/patent" }), _jsx(Row, { c: c, label: "Trademarks", value: "DPYC\u2122 \u00B7 Tollbooth DPYC\u2122 \u00B7 Don't Pester Your Customer\u2122" })] }));
|
|
49
|
+
}
|
|
@@ -12,16 +12,13 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
12
12
|
import { useCallback, useEffect, useState } from "react";
|
|
13
13
|
import { forgetCoupon, listMyCoupons, redeemCoupon } from "../standardTools.js";
|
|
14
14
|
import { couponExpiryText, couponStatusLabel, couponUsesText, redeemedText } from "../wallet.js";
|
|
15
|
+
import { cx } from "./cx.js";
|
|
15
16
|
const DEFAULT_INTRO = "Redeem an operator's code once. The discount applies on its own to later paid calls until its uses or its window run out.";
|
|
16
17
|
const DEFAULT_EMPTY = "No coupons yet. Paste a code above to claim its discount.";
|
|
17
18
|
function defaultConfirm() {
|
|
18
19
|
return (typeof window === "undefined" ||
|
|
19
20
|
window.confirm("Remove this coupon from your list? You can redeem the code again while its window allows."));
|
|
20
21
|
}
|
|
21
|
-
function cx(...parts) {
|
|
22
|
-
const s = parts.filter(Boolean).join(" ");
|
|
23
|
-
return s || undefined;
|
|
24
|
-
}
|
|
25
22
|
export default function CouponsPanel({ heading = "My coupons", intro = DEFAULT_INTRO, empty = DEFAULT_EMPTY, formHeading, listHeading, placeholder = "Coupon code", redeemLabel = "Redeem", forgetLabel = "Remove", confirmForget = defaultConfirm, classNames: c = {}, renderCoupon, }) {
|
|
26
23
|
const [coupons, setCoupons] = useState([]);
|
|
27
24
|
const [loading, setLoading] = useState(true);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Funding and credential health, as a list of rows that are read fresh and
|
|
3
|
+
* stamped with when: `PatronFundingStatus` (sign-in proof, credit balance and
|
|
4
|
+
* tranche expiry) and `OperatorFundingStatus` (credentials, Authority balance,
|
|
5
|
+
* database, background jobs).
|
|
6
|
+
*
|
|
7
|
+
* `OperatorFundingStatus` renders nothing — and asks nothing operator-only —
|
|
8
|
+
* unless the signed-in npub is the operator npub `session_status` reports, the
|
|
9
|
+
* rule the fleet's copies used. That decides only what the page shows; the
|
|
10
|
+
* server enforces its own ACL.
|
|
11
|
+
*
|
|
12
|
+
* Mechanics only: the calls, the rules (`funding.ts`) and the states. Every
|
|
13
|
+
* visual choice is the site's, through `classNames`, with `ok` / `warning` /
|
|
14
|
+
* `blocked` added to a row, its state word and the heading's summary mark.
|
|
15
|
+
* `siteRows` adds the site's own checks (an upstream OAuth connection, say);
|
|
16
|
+
* `renderRow` draws a row yourself. Refresh is a chip.
|
|
17
|
+
*/
|
|
18
|
+
import { type ReactNode } from "react";
|
|
19
|
+
import { type BalanceThresholds, type StatusLevel, type StatusRow } from "../funding.ts";
|
|
20
|
+
export interface FundingStatusClassNames {
|
|
21
|
+
root?: string;
|
|
22
|
+
/** The heading row: title, summary mark and the Refresh chip. */
|
|
23
|
+
header?: string;
|
|
24
|
+
heading?: string;
|
|
25
|
+
/** The summary mark: the worst state across rows. */
|
|
26
|
+
overall?: string;
|
|
27
|
+
chip?: string;
|
|
28
|
+
intro?: string;
|
|
29
|
+
list?: string;
|
|
30
|
+
row?: string;
|
|
31
|
+
dependency?: string;
|
|
32
|
+
state?: string;
|
|
33
|
+
detail?: string;
|
|
34
|
+
/** The "checked …" stamp. */
|
|
35
|
+
checked?: string;
|
|
36
|
+
loading?: string;
|
|
37
|
+
error?: string;
|
|
38
|
+
/** Added to a row, its state word and the summary mark, by state. */
|
|
39
|
+
ok?: string;
|
|
40
|
+
warning?: string;
|
|
41
|
+
blocked?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface FundingStatusProps {
|
|
44
|
+
heading?: ReactNode;
|
|
45
|
+
intro?: ReactNode;
|
|
46
|
+
/** The site's own checks, given the read time; drawn before the package's rows. */
|
|
47
|
+
siteRows?: (checkedAt: string) => Promise<StatusRow[]>;
|
|
48
|
+
/** The words for each state. Default "ok", "warning", "blocked". */
|
|
49
|
+
stateLabels?: Partial<Record<StatusLevel, ReactNode>>;
|
|
50
|
+
/** Draw a row yourself; `checked` is its read time in the patron's zone. */
|
|
51
|
+
renderRow?: (row: StatusRow, checked: string) => ReactNode;
|
|
52
|
+
/** Content of the Refresh chip; null for none. Default "Refresh". */
|
|
53
|
+
refreshLabel?: ReactNode;
|
|
54
|
+
classNames?: FundingStatusClassNames;
|
|
55
|
+
}
|
|
56
|
+
export interface PatronFundingStatusProps extends FundingStatusProps {
|
|
57
|
+
/** When the patron's balance reads as low or empty. */
|
|
58
|
+
thresholds?: BalanceThresholds;
|
|
59
|
+
}
|
|
60
|
+
export interface OperatorFundingStatusProps extends FundingStatusProps {
|
|
61
|
+
/** When the operator's Authority balance reads as low or empty. */
|
|
62
|
+
authorityThresholds?: BalanceThresholds;
|
|
63
|
+
}
|
|
64
|
+
/** The patron's own health: sign-in proof and credit balance. */
|
|
65
|
+
export declare function PatronFundingStatus({ thresholds, siteRows, heading, ...rest }: PatronFundingStatusProps): import("react").JSX.Element;
|
|
66
|
+
/** What only the operator can fix. Renders nothing for anyone else. */
|
|
67
|
+
export declare function OperatorFundingStatus({ authorityThresholds, siteRows, heading, ...rest }: OperatorFundingStatusProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Funding and credential health, as a list of rows that are read fresh and
|
|
4
|
+
* stamped with when: `PatronFundingStatus` (sign-in proof, credit balance and
|
|
5
|
+
* tranche expiry) and `OperatorFundingStatus` (credentials, Authority balance,
|
|
6
|
+
* database, background jobs).
|
|
7
|
+
*
|
|
8
|
+
* `OperatorFundingStatus` renders nothing — and asks nothing operator-only —
|
|
9
|
+
* unless the signed-in npub is the operator npub `session_status` reports, the
|
|
10
|
+
* rule the fleet's copies used. That decides only what the page shows; the
|
|
11
|
+
* server enforces its own ACL.
|
|
12
|
+
*
|
|
13
|
+
* Mechanics only: the calls, the rules (`funding.ts`) and the states. Every
|
|
14
|
+
* visual choice is the site's, through `classNames`, with `ok` / `warning` /
|
|
15
|
+
* `blocked` added to a row, its state word and the heading's summary mark.
|
|
16
|
+
* `siteRows` adds the site's own checks (an upstream OAuth connection, say);
|
|
17
|
+
* `renderRow` draws a row yourself. Refresh is a chip.
|
|
18
|
+
*/
|
|
19
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
20
|
+
import { composeAuthorityRow, composeCreditsRow, composeDurableJobsRow, composeOnboardingRow, composePersistenceRow, composeProofRow, isOperator, worstState, } from "../funding.js";
|
|
21
|
+
import { currentClaim } from "../identity.js";
|
|
22
|
+
import { canSignFor } from "../signedIn.js";
|
|
23
|
+
import { checkAuthorityBalance, checkBalance, checkProofStatus, getOperatorOnboardingStatus, serviceStatus, sessionStatus, } from "../standardTools.js";
|
|
24
|
+
import { formatDateTime } from "../timezone.js";
|
|
25
|
+
import { cx } from "./cx.js";
|
|
26
|
+
import { useTimezone } from "./useTimezone.js";
|
|
27
|
+
const STATE_LABELS = { ok: "ok", warning: "warning", blocked: "blocked" };
|
|
28
|
+
/**
|
|
29
|
+
* Rows from `read`, read on mount and on Refresh; null rows means "show
|
|
30
|
+
* nothing". The latest `read` is used, but a new one (a site passing
|
|
31
|
+
* `thresholds` or `siteRows` inline) does not trigger a re-read.
|
|
32
|
+
*/
|
|
33
|
+
function useRows(read) {
|
|
34
|
+
const latest = useRef(read);
|
|
35
|
+
latest.current = read;
|
|
36
|
+
const [rows, setRows] = useState(null);
|
|
37
|
+
const [loading, setLoading] = useState(true);
|
|
38
|
+
const [error, setError] = useState(null);
|
|
39
|
+
const refresh = useCallback(async () => {
|
|
40
|
+
setLoading(true);
|
|
41
|
+
setError(null);
|
|
42
|
+
try {
|
|
43
|
+
setRows(await latest.current());
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
setError(e.message);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
setLoading(false);
|
|
50
|
+
}
|
|
51
|
+
}, []);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
void refresh();
|
|
54
|
+
}, [refresh]);
|
|
55
|
+
return { rows, loading, error, refresh };
|
|
56
|
+
}
|
|
57
|
+
function StatusList({ heading, intro, stateLabels = {}, renderRow, refreshLabel = "Refresh", classNames: c = {}, rows, loading, error, refresh, }) {
|
|
58
|
+
const [, zone] = useTimezone();
|
|
59
|
+
const overall = worstState(rows);
|
|
60
|
+
return (_jsxs("section", { className: c.root, children: [_jsxs("div", { className: c.header, children: [heading && _jsx("div", { className: c.heading, children: heading }), rows.length > 0 && !loading && (_jsx("span", { className: cx(c.overall, c[overall]), children: stateLabels[overall] ?? STATE_LABELS[overall] })), refreshLabel !== null && (_jsx("button", { type: "button", onClick: refresh, disabled: loading, className: c.chip, children: loading ? "Checking…" : refreshLabel }))] }), intro && _jsx("p", { className: c.intro, children: intro }), error && _jsx("p", { className: c.error, children: error }), loading && rows.length === 0 && _jsx("p", { className: c.loading, children: "Checking\u2026" }), rows.length > 0 && (_jsx("ul", { className: c.list, children: rows.map((r) => {
|
|
61
|
+
const checked = formatDateTime(r.checked_at, zone);
|
|
62
|
+
return (_jsx("li", { className: cx(c.row, c[r.state]), children: renderRow ? (renderRow(r, checked)) : (_jsxs(_Fragment, { children: [_jsx("span", { className: c.dependency, children: r.dependency }), " ", _jsx("span", { className: cx(c.state, c[r.state]), children: stateLabels[r.state] ?? STATE_LABELS[r.state] }), _jsx("p", { className: c.detail, children: r.detail }), _jsxs("p", { className: c.checked, children: ["checked ", checked] })] })) }, r.id));
|
|
63
|
+
}) }))] }));
|
|
64
|
+
}
|
|
65
|
+
function failedBalance(e) {
|
|
66
|
+
return { success: false, error: e.message };
|
|
67
|
+
}
|
|
68
|
+
async function proofKind() {
|
|
69
|
+
const claim = currentClaim();
|
|
70
|
+
if (canSignFor(claim))
|
|
71
|
+
return { kind: "session_nsec" };
|
|
72
|
+
if (!claim.proof)
|
|
73
|
+
return { kind: "none" };
|
|
74
|
+
const s = await checkProofStatus(claim.npub, claim.proof).catch(() => null);
|
|
75
|
+
const status = s?.status === "valid" || s?.status === "expired" ? s.status : "unknown";
|
|
76
|
+
return { kind: "dm_proof", status, expiresInSec: s?.expires_in_seconds ?? null };
|
|
77
|
+
}
|
|
78
|
+
/** The patron's own health: sign-in proof and credit balance. */
|
|
79
|
+
export function PatronFundingStatus({ thresholds, siteRows, heading = "Account health", ...rest }) {
|
|
80
|
+
const read = async () => {
|
|
81
|
+
const checkedAt = new Date().toISOString();
|
|
82
|
+
const [site, proof, bal] = await Promise.all([
|
|
83
|
+
siteRows ? siteRows(checkedAt) : Promise.resolve([]),
|
|
84
|
+
proofKind(),
|
|
85
|
+
checkBalance().catch(failedBalance),
|
|
86
|
+
]);
|
|
87
|
+
return [...site, composeProofRow(proof, checkedAt), composeCreditsRow(bal, checkedAt, thresholds)];
|
|
88
|
+
};
|
|
89
|
+
const state = useRows(read);
|
|
90
|
+
return _jsx(StatusList, { heading: heading, ...rest, ...state, rows: state.rows ?? [], refresh: () => void state.refresh() });
|
|
91
|
+
}
|
|
92
|
+
/** What only the operator can fix. Renders nothing for anyone else. */
|
|
93
|
+
export function OperatorFundingStatus({ authorityThresholds, siteRows, heading = "Operator dependencies", ...rest }) {
|
|
94
|
+
const read = async () => {
|
|
95
|
+
const checkedAt = new Date().toISOString();
|
|
96
|
+
// Gate first: a patron fires none of the operator's probes.
|
|
97
|
+
const life = await sessionStatus();
|
|
98
|
+
if (!isOperator(currentClaim().npub, life))
|
|
99
|
+
return null;
|
|
100
|
+
const [site, svc, onb, auth] = await Promise.all([
|
|
101
|
+
siteRows ? siteRows(checkedAt) : Promise.resolve([]),
|
|
102
|
+
serviceStatus().catch(() => ({})),
|
|
103
|
+
getOperatorOnboardingStatus().catch((e) => ({ error: e.message })),
|
|
104
|
+
checkAuthorityBalance().catch(failedBalance),
|
|
105
|
+
]);
|
|
106
|
+
const jobs = composeDurableJobsRow(svc, checkedAt);
|
|
107
|
+
return [
|
|
108
|
+
...site,
|
|
109
|
+
composeOnboardingRow(onb, checkedAt),
|
|
110
|
+
composeAuthorityRow(auth, checkedAt, authorityThresholds),
|
|
111
|
+
composePersistenceRow(life, svc, onb, checkedAt),
|
|
112
|
+
...(jobs ? [jobs] : []),
|
|
113
|
+
];
|
|
114
|
+
};
|
|
115
|
+
const state = useRows(read);
|
|
116
|
+
// Nothing until the viewer is known to be the operator — no flash for patrons.
|
|
117
|
+
if (state.rows === null)
|
|
118
|
+
return null;
|
|
119
|
+
return _jsx(StatusList, { heading: heading, ...rest, ...state, rows: state.rows, refresh: () => void state.refresh() });
|
|
120
|
+
}
|
package/dist/react/PagedTable.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ariaSort, clampPage, lastPage, nextSort } from "../table.js";
|
|
3
|
-
|
|
4
|
-
const s = parts.filter(Boolean).join(" ");
|
|
5
|
-
return s || undefined;
|
|
6
|
-
}
|
|
3
|
+
import { cx } from "./cx.js";
|
|
7
4
|
/** A header cell. Tapping the sorted column flips it; another column starts at `initialDir`. */
|
|
8
5
|
export function SortHeader({ label, col, activeCol, dir, onSort, initialDir = "desc", as = "th", classNames: c = {}, }) {
|
|
9
6
|
const Cell = as;
|
|
@@ -19,12 +19,9 @@ import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
|
19
19
|
import { Search, SlidersHorizontal, X } from "lucide-react";
|
|
20
20
|
import { filterActive } from "../table.js";
|
|
21
21
|
import { oncePerFrame, reclampPanel } from "./panelPlacement.js";
|
|
22
|
+
import { cx } from "./cx.js";
|
|
22
23
|
/** The panel keeps this many px from each edge of the viewport. */
|
|
23
24
|
const EDGE = 8;
|
|
24
|
-
function cx(...parts) {
|
|
25
|
-
const s = parts.filter(Boolean).join(" ");
|
|
26
|
-
return s || undefined;
|
|
27
|
-
}
|
|
28
25
|
export default function TableFilter({ search, dates, questions, onClear, clearPlacement = "beside", searchIcon = _jsx(Search, { size: "1em", "aria-hidden": true }), clearLabel = (_jsxs(_Fragment, { children: [_jsx(X, { size: "1em", "aria-hidden": true }), " Clear"] })), classNames: c = {}, }) {
|
|
29
26
|
const active = filterActive({ search: search?.value, dateFrom: dates?.from, dateTo: dates?.to }, questions?.summary ?? "");
|
|
30
27
|
const clear = active ? (_jsx("button", { type: "button", onClick: onClear, className: c.chip, children: clearLabel })) : null;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The display time zone as a select: "Automatic (<the browser's zone>)" and a
|
|
3
|
+
* list of zones. Choosing one stores it under `<prefix>:timezone` and every
|
|
4
|
+
* clock using `useTimezone` follows. Mechanics only — `classNames` styles it,
|
|
5
|
+
* `autoLabel` / `optionLabel` word it, `options` picks the zones offered. A
|
|
6
|
+
* stored zone that is not in `options` is still shown, so the select never
|
|
7
|
+
* lies about the pick.
|
|
8
|
+
*/
|
|
9
|
+
export interface TimezoneOption {
|
|
10
|
+
value: string;
|
|
11
|
+
label: string;
|
|
12
|
+
}
|
|
13
|
+
export interface TimezonePickerClassNames {
|
|
14
|
+
root?: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
select?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface TimezonePickerProps {
|
|
19
|
+
/** The zones offered. Default `TIMEZONE_OPTIONS` (major cities and UTC). */
|
|
20
|
+
options?: readonly TimezoneOption[];
|
|
21
|
+
/** A visible label above the select; without one the select is labelled "Time zone". */
|
|
22
|
+
label?: string;
|
|
23
|
+
/** Text of the "auto" choice, given the browser's zone. Default "Automatic (<zone>)". */
|
|
24
|
+
autoLabel?: (detected: string) => string;
|
|
25
|
+
/** Text of each zone. Default its label. */
|
|
26
|
+
optionLabel?: (option: TimezoneOption) => string;
|
|
27
|
+
id?: string;
|
|
28
|
+
classNames?: TimezonePickerClassNames;
|
|
29
|
+
}
|
|
30
|
+
export default function TimezonePicker({ options, label, autoLabel, optionLabel, id, classNames: c, }: TimezonePickerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The display time zone as a select: "Automatic (<the browser's zone>)" and a
|
|
4
|
+
* list of zones. Choosing one stores it under `<prefix>:timezone` and every
|
|
5
|
+
* clock using `useTimezone` follows. Mechanics only — `classNames` styles it,
|
|
6
|
+
* `autoLabel` / `optionLabel` word it, `options` picks the zones offered. A
|
|
7
|
+
* stored zone that is not in `options` is still shown, so the select never
|
|
8
|
+
* lies about the pick.
|
|
9
|
+
*/
|
|
10
|
+
import { TIMEZONE_OPTIONS, detectBrowserTimeZone } from "../timezone.js";
|
|
11
|
+
import { useTimezone } from "./useTimezone.js";
|
|
12
|
+
const defaultAuto = (detected) => `Automatic (${detected})`;
|
|
13
|
+
const defaultOption = (o) => o.label;
|
|
14
|
+
export default function TimezonePicker({ options = TIMEZONE_OPTIONS, label, autoLabel = defaultAuto, optionLabel = defaultOption, id = "tb-timezone", classNames: c = {}, }) {
|
|
15
|
+
const [pref, , setPref] = useTimezone();
|
|
16
|
+
const listed = pref === "auto" || options.some((o) => o.value === pref);
|
|
17
|
+
const shown = listed ? options : [...options, { value: pref, label: pref }];
|
|
18
|
+
return (_jsxs("div", { className: c.root, children: [label && (_jsx("label", { htmlFor: id, className: c.label, children: label })), _jsxs("select", { id: id, value: pref, onChange: (e) => setPref(e.target.value), "aria-label": label ? undefined : "Time zone", className: c.select, children: [_jsx("option", { value: "auto", children: autoLabel(detectBrowserTimeZone()) }), shown.map((o) => (_jsx("option", { value: o.value, children: optionLabel(o) }, o.value)))] })] }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The account-statement card: balance and lifetime totals, what the last N
|
|
3
|
+
* days spent and bought, and the tools the sats went to.
|
|
4
|
+
*
|
|
5
|
+
* Mechanics only — the `account_statement` call, loading and error states and
|
|
6
|
+
* the arithmetic (`usageFacts`). Every visual choice is the site's, through
|
|
7
|
+
* `classNames`; `figures` picks which numbers show and in what order, `labels`
|
|
8
|
+
* words them, and `renderRow` draws a tool row yourself (a display name for a
|
|
9
|
+
* raw tool name, say). Refresh is a chip.
|
|
10
|
+
*/
|
|
11
|
+
import { type ReactNode } from "react";
|
|
12
|
+
import { type ToolSpend } from "../usage.ts";
|
|
13
|
+
export type UsageFigure = "balance" | "deposited" | "consumed" | "expired" | "spent" | "calls" | "credited";
|
|
14
|
+
export interface UsageSummaryClassNames {
|
|
15
|
+
root?: string;
|
|
16
|
+
/** The heading row: title and the Refresh chip. */
|
|
17
|
+
header?: string;
|
|
18
|
+
heading?: string;
|
|
19
|
+
chip?: string;
|
|
20
|
+
figures?: string;
|
|
21
|
+
figure?: string;
|
|
22
|
+
value?: string;
|
|
23
|
+
label?: string;
|
|
24
|
+
/** The heading over the tools. */
|
|
25
|
+
subheading?: string;
|
|
26
|
+
list?: string;
|
|
27
|
+
row?: string;
|
|
28
|
+
tool?: string;
|
|
29
|
+
calls?: string;
|
|
30
|
+
sats?: string;
|
|
31
|
+
loading?: string;
|
|
32
|
+
error?: string;
|
|
33
|
+
empty?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface UsageSummaryProps {
|
|
36
|
+
/** The period asked of the server. Default 30. */
|
|
37
|
+
days?: number;
|
|
38
|
+
/** Default "Last <days> days". */
|
|
39
|
+
heading?: ReactNode;
|
|
40
|
+
/** The numbers shown, in order. Default all; "expired" only when above zero. */
|
|
41
|
+
figures?: readonly UsageFigure[];
|
|
42
|
+
labels?: Partial<Record<UsageFigure, ReactNode>>;
|
|
43
|
+
/** How many tools to list. 0 hides the list. Default 5. */
|
|
44
|
+
topTools?: number;
|
|
45
|
+
/** Default "Top tools". */
|
|
46
|
+
toolsHeading?: ReactNode;
|
|
47
|
+
/** Draw a tool row yourself. */
|
|
48
|
+
renderRow?: (tool: ToolSpend) => ReactNode;
|
|
49
|
+
/** Shown when the period has no paid calls. Default "No paid calls in this period." */
|
|
50
|
+
empty?: ReactNode;
|
|
51
|
+
/** Content of the Refresh chip; null for none. Default "Refresh". */
|
|
52
|
+
refreshLabel?: ReactNode;
|
|
53
|
+
classNames?: UsageSummaryClassNames;
|
|
54
|
+
}
|
|
55
|
+
export default function UsageSummary({ days, heading, figures, labels, topTools, toolsHeading, renderRow, empty, refreshLabel, classNames: c, }: UsageSummaryProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The account-statement card: balance and lifetime totals, what the last N
|
|
4
|
+
* days spent and bought, and the tools the sats went to.
|
|
5
|
+
*
|
|
6
|
+
* Mechanics only — the `account_statement` call, loading and error states and
|
|
7
|
+
* the arithmetic (`usageFacts`). Every visual choice is the site's, through
|
|
8
|
+
* `classNames`; `figures` picks which numbers show and in what order, `labels`
|
|
9
|
+
* words them, and `renderRow` draws a tool row yourself (a display name for a
|
|
10
|
+
* raw tool name, say). Refresh is a chip.
|
|
11
|
+
*/
|
|
12
|
+
import { useCallback, useEffect, useState } from "react";
|
|
13
|
+
import { getAccountStatement } from "../standardTools.js";
|
|
14
|
+
import { usageFacts } from "../usage.js";
|
|
15
|
+
const ALL = ["balance", "deposited", "consumed", "expired", "spent", "calls", "credited"];
|
|
16
|
+
const LABELS = {
|
|
17
|
+
balance: "Balance",
|
|
18
|
+
deposited: "Deposited",
|
|
19
|
+
consumed: "Consumed",
|
|
20
|
+
expired: "Expired",
|
|
21
|
+
spent: "Spent",
|
|
22
|
+
calls: "Calls",
|
|
23
|
+
credited: "Bought",
|
|
24
|
+
};
|
|
25
|
+
export default function UsageSummary({ days = 30, heading, figures = ALL, labels = {}, topTools = 5, toolsHeading = "Top tools", renderRow, empty = "No paid calls in this period.", refreshLabel = "Refresh", classNames: c = {}, }) {
|
|
26
|
+
const [facts, setFacts] = useState(null);
|
|
27
|
+
const [loading, setLoading] = useState(true);
|
|
28
|
+
const [error, setError] = useState(null);
|
|
29
|
+
const load = useCallback(async () => {
|
|
30
|
+
setLoading(true);
|
|
31
|
+
setError(null);
|
|
32
|
+
try {
|
|
33
|
+
const stmt = await getAccountStatement(days);
|
|
34
|
+
const f = usageFacts(stmt, days, topTools);
|
|
35
|
+
if (f)
|
|
36
|
+
setFacts(f);
|
|
37
|
+
else
|
|
38
|
+
setError(stmt.error ?? "No statement available.");
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
setError(e.message);
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}
|
|
46
|
+
}, [days, topTools]);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
void load();
|
|
49
|
+
}, [load]);
|
|
50
|
+
const shown = facts ? figures.filter((f) => f !== "expired" || facts.expired > 0) : [];
|
|
51
|
+
return (_jsxs("section", { className: c.root, children: [_jsxs("div", { className: c.header, children: [_jsx("div", { className: c.heading, children: heading ?? `Last ${days} days` }), refreshLabel !== null && (_jsx("button", { type: "button", onClick: () => void load(), disabled: loading, className: c.chip, children: loading ? "Loading…" : refreshLabel }))] }), error ? (_jsx("p", { className: c.error, children: error })) : !facts ? (loading && _jsx("p", { className: c.loading, children: "Loading\u2026" })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: c.figures, children: shown.map((f) => (_jsxs("div", { className: c.figure, children: [_jsx("div", { className: c.value, children: facts[f].toLocaleString() }), _jsx("div", { className: c.label, children: labels[f] ?? LABELS[f] })] }, f))) }), topTools > 0 &&
|
|
52
|
+
(facts.tools.length === 0 ? (empty && _jsx("p", { className: c.empty, children: empty })) : (_jsxs(_Fragment, { children: [toolsHeading && _jsx("div", { className: c.subheading, children: toolsHeading }), _jsx("ul", { className: c.list, children: facts.tools.map((t) => (_jsx("li", { className: c.row, children: renderRow ? (renderRow(t)) : (_jsxs(_Fragment, { children: [_jsx("span", { className: c.tool, children: t.tool }), " ", _jsxs("span", { className: c.calls, children: [t.calls.toLocaleString(), " call", t.calls === 1 ? "" : "s"] }), " ", _jsxs("span", { className: c.sats, children: [t.sats.toLocaleString(), " sats"] })] })) }, t.tool))) })] })))] }))] }));
|
|
53
|
+
}
|
package/dist/react/WalletPage.js
CHANGED
|
@@ -20,13 +20,10 @@ import { checkBalance, getAccountStatement, } from "../standardTools.js";
|
|
|
20
20
|
import { amountReducer, formatSats, parseSats, presetChosen } from "../wallet.js";
|
|
21
21
|
import CouponsPanel, {} from "./CouponsPanel.js";
|
|
22
22
|
import { useTopUp } from "./useTopUp.js";
|
|
23
|
+
import { cx } from "./cx.js";
|
|
23
24
|
const DEFAULT_TOP_UPS = [1_000, 5_000, 25_000];
|
|
24
25
|
const localDate = (iso) => new Date(iso).toLocaleDateString();
|
|
25
26
|
const localDateTime = (iso) => new Date(iso).toLocaleString();
|
|
26
|
-
function cx(...parts) {
|
|
27
|
-
const s = parts.filter(Boolean).join(" ");
|
|
28
|
-
return s || undefined;
|
|
29
|
-
}
|
|
30
27
|
export default function WalletPage({ heading = "Wallet", topUps = DEFAULT_TOP_UPS, customAmount = true, poll, coupons = true, statement = true, formatDate = localDate, formatDateTime = localDateTime, before, children, classNames: c = {}, }) {
|
|
31
28
|
const [bal, setBal] = useState(null);
|
|
32
29
|
const [balError, setBalError] = useState(null);
|
package/dist/react/cx.js
ADDED
package/dist/react/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export { default as Avatar } from "./Avatar.tsx";
|
|
2
2
|
export { default as AvatarPicker } from "./AvatarPicker.tsx";
|
|
3
|
+
export { default as BuildInfoPanel, type BuildInfoFrontend, type BuildInfoLicence, type BuildInfoPanelClassNames, type BuildInfoPanelProps, } from "./BuildInfoPanel.tsx";
|
|
3
4
|
export { default as CouponsPanel, type CouponsPanelProps, type CouponsPanelClassNames } from "./CouponsPanel.tsx";
|
|
4
5
|
export { default as DebugPanel, type DebugPanelProps } from "./DebugPanel.tsx";
|
|
5
6
|
export { default as ErrorBoundary, type ErrorBoundaryProps, type ErrorBoundaryClassNames, type ErrorFallbackProps, } from "./ErrorBoundary.tsx";
|
|
7
|
+
export { OperatorFundingStatus, PatronFundingStatus, type FundingStatusClassNames, type FundingStatusProps, type OperatorFundingStatusProps, type PatronFundingStatusProps, } from "./FundingStatus.tsx";
|
|
6
8
|
export { default as NpubGate } from "./NpubGate.tsx";
|
|
7
9
|
export { default as NostrProfilePanel } from "./NostrProfilePanel.tsx";
|
|
8
10
|
export { PageControls, SortHeader, TableShell, type PageControlsProps, type PageControlsClassNames, type SortHeaderProps, type SortHeaderClassNames, type TableShellClassNames, } from "./PagedTable.tsx";
|
|
@@ -10,9 +12,12 @@ export { default as QuoteScroller, type QuoteScrollerProps, type QuoteScrollerCl
|
|
|
10
12
|
export { default as SessionKeyClaim } from "./SessionKeyClaim.tsx";
|
|
11
13
|
export { default as TableFilter, type DateFieldOption, type FilterQuestion, type TableFilterProps, type TableFilterClassNames, type TableFilterDates, type TableFilterQuestions, type TableFilterSearch, } from "./TableFilter.tsx";
|
|
12
14
|
export { default as ThemeToggle, type ThemeToggleProps, type ThemeToggleClassNames } from "./ThemeToggle.tsx";
|
|
15
|
+
export { default as TimezonePicker, type TimezoneOption, type TimezonePickerClassNames, type TimezonePickerProps, } from "./TimezonePicker.tsx";
|
|
16
|
+
export { default as UsageSummary, type UsageFigure, type UsageSummaryClassNames, type UsageSummaryProps, } from "./UsageSummary.tsx";
|
|
13
17
|
export { default as WalletCard, type WalletCardProps, type WalletCardClassNames } from "./WalletCard.tsx";
|
|
14
18
|
export { default as WalletPage, type WalletPageProps, type WalletPageClassNames } from "./WalletPage.tsx";
|
|
15
19
|
export { useSession, type Session } from "./useSession.ts";
|
|
16
20
|
export { useDebugLog } from "./useDebugLog.ts";
|
|
17
21
|
export { useTheme } from "./useTheme.ts";
|
|
22
|
+
export { useTimezone } from "./useTimezone.ts";
|
|
18
23
|
export { useTopUp, type TopUp, type UseTopUpOptions } from "./useTopUp.ts";
|
package/dist/react/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export { default as Avatar } from "./Avatar.js";
|
|
2
2
|
export { default as AvatarPicker } from "./AvatarPicker.js";
|
|
3
|
+
export { default as BuildInfoPanel, } from "./BuildInfoPanel.js";
|
|
3
4
|
export { default as CouponsPanel } from "./CouponsPanel.js";
|
|
4
5
|
export { default as DebugPanel } from "./DebugPanel.js";
|
|
5
6
|
export { default as ErrorBoundary, } from "./ErrorBoundary.js";
|
|
7
|
+
export { OperatorFundingStatus, PatronFundingStatus, } from "./FundingStatus.js";
|
|
6
8
|
export { default as NpubGate } from "./NpubGate.js";
|
|
7
9
|
export { default as NostrProfilePanel } from "./NostrProfilePanel.js";
|
|
8
10
|
export { PageControls, SortHeader, TableShell, } from "./PagedTable.js";
|
|
@@ -10,9 +12,12 @@ export { default as QuoteScroller } from "./QuoteScroller.js";
|
|
|
10
12
|
export { default as SessionKeyClaim } from "./SessionKeyClaim.js";
|
|
11
13
|
export { default as TableFilter, } from "./TableFilter.js";
|
|
12
14
|
export { default as ThemeToggle } from "./ThemeToggle.js";
|
|
15
|
+
export { default as TimezonePicker, } from "./TimezonePicker.js";
|
|
16
|
+
export { default as UsageSummary, } from "./UsageSummary.js";
|
|
13
17
|
export { default as WalletCard } from "./WalletCard.js";
|
|
14
18
|
export { default as WalletPage } from "./WalletPage.js";
|
|
15
19
|
export { useSession } from "./useSession.js";
|
|
16
20
|
export { useDebugLog } from "./useDebugLog.js";
|
|
17
21
|
export { useTheme } from "./useTheme.js";
|
|
22
|
+
export { useTimezone } from "./useTimezone.js";
|
|
18
23
|
export { useTopUp } from "./useTopUp.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The display time zone as React state: `[pref, zone, setPref]` — the stored
|
|
3
|
+
* preference ("auto" or an IANA name), the zone it resolves to now, and a
|
|
4
|
+
* setter. Every component using it re-renders when any of them sets it, and
|
|
5
|
+
* when another tab does.
|
|
6
|
+
*/
|
|
7
|
+
import { type TimezonePref } from "../timezone.ts";
|
|
8
|
+
export declare function useTimezone(): [TimezonePref, string, (next: TimezonePref) => void];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The display time zone as React state: `[pref, zone, setPref]` — the stored
|
|
3
|
+
* preference ("auto" or an IANA name), the zone it resolves to now, and a
|
|
4
|
+
* setter. Every component using it re-renders when any of them sets it, and
|
|
5
|
+
* when another tab does.
|
|
6
|
+
*/
|
|
7
|
+
import { useCallback, useMemo, useSyncExternalStore } from "react";
|
|
8
|
+
import { onTimezoneChange, readTimezonePref, resolveTimeZone, timezoneStorageKey, writeTimezonePref, } from "../timezone.js";
|
|
9
|
+
function subscribe(changed) {
|
|
10
|
+
const stopPage = onTimezoneChange(changed);
|
|
11
|
+
const onStorage = (e) => {
|
|
12
|
+
try {
|
|
13
|
+
if (e.key === timezoneStorageKey())
|
|
14
|
+
changed();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
/* not configured yet: nothing of ours changed */
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
globalThis.addEventListener?.("storage", onStorage);
|
|
21
|
+
return () => {
|
|
22
|
+
stopPage();
|
|
23
|
+
globalThis.removeEventListener?.("storage", onStorage);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const onServer = () => "auto";
|
|
27
|
+
export function useTimezone() {
|
|
28
|
+
const pref = useSyncExternalStore(subscribe, readTimezonePref, onServer);
|
|
29
|
+
const zone = useMemo(() => resolveTimeZone(pref), [pref]);
|
|
30
|
+
const setPref = useCallback((next) => {
|
|
31
|
+
writeTimezonePref(next);
|
|
32
|
+
}, []);
|
|
33
|
+
return [pref, zone, setPref];
|
|
34
|
+
}
|