@tollbooth-dpyc/web 1.3.1 → 1.4.0
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/debugLog.js +3 -12
- 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,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
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The patron's display time zone, and the conversions every clock on a page
|
|
3
|
+
* needs. Storage stays UTC; this converts at the edges — rendering an instant,
|
|
4
|
+
* reading an <input type="datetime-local">, turning a day filter into bounds.
|
|
5
|
+
*
|
|
6
|
+
* The preference is "auto" (the browser's own zone) or an IANA name, kept under
|
|
7
|
+
* `<prefix>:timezone`. Never a fixed offset: an offset is right for half the
|
|
8
|
+
* year. A stored name the browser does not know reads as "auto".
|
|
9
|
+
*
|
|
10
|
+
* Pure apart from the preference's storage, so it is tested without a browser.
|
|
11
|
+
* Lifted from eXcalibur's `lib/timezone.ts` (roastify held a copy).
|
|
12
|
+
*/
|
|
13
|
+
export type TimezonePref = "auto" | (string & {});
|
|
14
|
+
/** A starter set of major cities, west to east, plus UTC. Values are IANA names. */
|
|
15
|
+
export declare const TIMEZONE_OPTIONS: readonly {
|
|
16
|
+
value: string;
|
|
17
|
+
label: string;
|
|
18
|
+
}[];
|
|
19
|
+
/** The browser's own zone, or UTC when it will not say. */
|
|
20
|
+
export declare function detectBrowserTimeZone(): string;
|
|
21
|
+
/** True when `tz` is a zone name this browser knows. An offset ("+05:00") is not. */
|
|
22
|
+
export declare function isValidTimeZone(tz: string): boolean;
|
|
23
|
+
/** "auto", or the name when the browser knows it; anything else is "auto". */
|
|
24
|
+
export declare function normalizeTimezonePref(pref: string | null | undefined): TimezonePref;
|
|
25
|
+
/** The IANA zone a preference means right now. */
|
|
26
|
+
export declare function resolveTimeZone(pref: TimezonePref): string;
|
|
27
|
+
/** `<prefix>:timezone` — for a `storage` event listener. */
|
|
28
|
+
export declare function timezoneStorageKey(): string;
|
|
29
|
+
export declare function readTimezonePref(): TimezonePref;
|
|
30
|
+
/** Store the preference (normalized) and tell every listener on the page. */
|
|
31
|
+
export declare function writeTimezonePref(next: string): TimezonePref;
|
|
32
|
+
/** Hear every `writeTimezonePref` on this page. Returns an unsubscribe. */
|
|
33
|
+
export declare function onTimezoneChange(fn: (pref: TimezonePref) => void): () => void;
|
|
34
|
+
/** The zone every clock should use: the stored preference, resolved. */
|
|
35
|
+
export declare function displayTimeZone(): string;
|
|
36
|
+
export interface ZonedParts {
|
|
37
|
+
year: number;
|
|
38
|
+
/** 1–12 */
|
|
39
|
+
month: number;
|
|
40
|
+
day: number;
|
|
41
|
+
/** 0–23 */
|
|
42
|
+
hour: number;
|
|
43
|
+
minute: number;
|
|
44
|
+
second: number;
|
|
45
|
+
}
|
|
46
|
+
/** The calendar and clock of an instant as seen in `timeZone`. */
|
|
47
|
+
export declare function getZonedParts(date: Date, timeZone: string): ZonedParts;
|
|
48
|
+
/** The zone's offset from UTC at an instant, in ms (New York in winter: −5 h). */
|
|
49
|
+
export declare function getTimeZoneOffsetMs(date: Date, timeZone: string): number;
|
|
50
|
+
/**
|
|
51
|
+
* A wall time in `timeZone` as a UTC instant (epoch ms).
|
|
52
|
+
*
|
|
53
|
+
* Around a DST change a wall time can be missing or doubled. A missing one
|
|
54
|
+
* (02:30 on New York's spring-forward day) is read with the offset from before
|
|
55
|
+
* the change, so it lands an hour later (03:30); a doubled one (01:30 on the
|
|
56
|
+
* fall-back day) is the earlier of the two. Temporal calls this "compatible".
|
|
57
|
+
*/
|
|
58
|
+
export declare function zonedWallTimeToUtcMs(year: number, month: number, day: number, hour: number, minute: number, second: number, timeZone: string): number;
|
|
59
|
+
export declare function formatDateTime(iso: string | null | undefined, timeZone: string, opts?: Intl.DateTimeFormatOptions): string;
|
|
60
|
+
export declare function formatDate(iso: string | null | undefined, timeZone: string, opts?: Intl.DateTimeFormatOptions): string;
|
|
61
|
+
export declare function formatTime(iso: string | null | undefined, timeZone: string, opts?: Intl.DateTimeFormatOptions): string;
|
|
62
|
+
/** An hour of the day (0–23) as the patron's locale writes it: "9 AM", "21". */
|
|
63
|
+
export declare function formatHourLabel(hour: number, timeZone: string): string;
|
|
64
|
+
/** The hour (0–23) of an instant in `timeZone`, or null for a bad instant. */
|
|
65
|
+
export declare function hourInZone(iso: string, timeZone: string): number | null;
|
|
66
|
+
/** A UTC instant → the value for a datetime-local input showing `timeZone`. */
|
|
67
|
+
export declare function isoToDatetimeLocalValue(iso: string, timeZone: string): string;
|
|
68
|
+
/** A datetime-local value, read as a wall time in `timeZone` → UTC ISO. */
|
|
69
|
+
export declare function datetimeLocalValueToIso(value: string, timeZone: string): string | null;
|
|
70
|
+
/** The patron's midnight starting `day`, as UTC ISO (an inclusive lower bound). */
|
|
71
|
+
export declare function startOfLocalDayIso(day: string, timeZone: string): string | null;
|
|
72
|
+
/** The patron's midnight after `day`, as UTC ISO (an exclusive upper bound). */
|
|
73
|
+
export declare function startOfNextLocalDayIso(day: string, timeZone: string): string | null;
|
|
74
|
+
/**
|
|
75
|
+
* A from/to day filter as the instants a server can compare: from the patron's
|
|
76
|
+
* midnight starting `dateFrom` up to (not including) the one after `dateTo`.
|
|
77
|
+
* An empty or malformed day is left out.
|
|
78
|
+
*/
|
|
79
|
+
export declare function localDateFilterBounds(dateFrom: string, dateTo: string, timeZone: string): {
|
|
80
|
+
dateFrom?: string;
|
|
81
|
+
dateTo?: string;
|
|
82
|
+
};
|