This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
"use client"
|
||||
|
||||
// "Export my data": one call, and the answer saved as a JSON file in the browser. A section that
|
||||
// could not be exported is named on the page as well as in the file, so a person never takes a
|
||||
// partial export for a whole one.
|
||||
|
||||
import { useTransport } from "@conusai-kit/ui"
|
||||
import { type ReactElement, useId, useState } from "react"
|
||||
import type { AccountExport } from "../contract/procedures.js"
|
||||
|
||||
export interface AccountExportLabels {
|
||||
readonly heading: string
|
||||
readonly body: string
|
||||
readonly submit: string
|
||||
readonly busy: string
|
||||
readonly done: string
|
||||
/** With `{sections}` still in it. */
|
||||
readonly partial: string
|
||||
readonly filename: string
|
||||
/** The app's `app.failed` message, with `{detail}` still in it. */
|
||||
readonly failed: string
|
||||
/** A readable name per section key; a key without one is shown as it is. */
|
||||
readonly sectionNames: Readonly<Record<string, string>>
|
||||
}
|
||||
|
||||
type State =
|
||||
| { readonly kind: "idle" | "busy" | "done" }
|
||||
| { readonly kind: "partial"; readonly sections: string }
|
||||
| { readonly kind: "failed"; readonly detail: string }
|
||||
|
||||
function save(exported: AccountExport, filename: string): void {
|
||||
const href = URL.createObjectURL(new Blob([JSON.stringify(exported, null, 2)], { type: "application/json" }))
|
||||
const link = document.createElement("a")
|
||||
link.href = href
|
||||
link.download = `${filename}-${exported.exportedAt.slice(0, 10)}.json`
|
||||
document.body.append(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
setTimeout(() => URL.revokeObjectURL(href), 0)
|
||||
}
|
||||
|
||||
export function AccountExportPanel({ labels }: { labels: AccountExportLabels }): ReactElement {
|
||||
const { call, describe } = useTransport()
|
||||
const headingId = useId()
|
||||
const [state, setState] = useState<State>({ kind: "idle" })
|
||||
|
||||
const run = async (): Promise<void> => {
|
||||
setState({ kind: "busy" })
|
||||
try {
|
||||
const exported = (await call("account.data.export", {})) as AccountExport
|
||||
save(exported, labels.filename)
|
||||
const missing = Object.entries(exported.sections)
|
||||
.filter(([, section]) => section.status !== "ok")
|
||||
.map(([key]) => labels.sectionNames[key] ?? key)
|
||||
setState(exported.complete ? { kind: "done" } : { kind: "partial", sections: missing.join(", ") })
|
||||
} catch (error) {
|
||||
setState({ kind: "failed", detail: describe(error) })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section aria-labelledby={headingId}>
|
||||
<h2 id={headingId}>{labels.heading}</h2>
|
||||
<p>{labels.body}</p>
|
||||
<button type="button" onClick={run} disabled={state.kind === "busy"} aria-busy={state.kind === "busy"}>
|
||||
{state.kind === "busy" ? labels.busy : labels.submit}
|
||||
</button>
|
||||
{state.kind === "done" ? <p role="status">{labels.done}</p> : null}
|
||||
{state.kind === "partial" ? (
|
||||
<p role="alert" className="app-error">
|
||||
{labels.partial.replace("{sections}", state.sections)}
|
||||
</p>
|
||||
) : null}
|
||||
{state.kind === "failed" ? (
|
||||
<p role="alert" className="app-error">
|
||||
{labels.failed.replace("{detail}", state.detail)}
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user