"use client" // Download the approved lines as an Excel workbook. The procedure answers with the file as base64 // (a procedure's answer is JSON on every transport); the browser turns it into a download. With // nothing approved the button is disabled and says why, in text a screen reader reads with it. import { type ReactElement, useId, useState } from "react" import type { ProjectSlugs } from "./slugs.js" import { useTransport } from "./transport.js" export interface ExportLabels { readonly export: string readonly hint: string readonly nothing: string /** Said after the workbook was handed to the browser. */ readonly exported: string readonly failed: string } export function ExportButton({ slugs, fileId, approved, labels, }: { slugs: ProjectSlugs fileId: string /** How many lines are approved: with none, there is nothing to export. */ approved: number labels: ExportLabels }): ReactElement { const { call, describe } = useTransport() const hintId = useId() const [message, setMessage] = useState<{ ok: boolean; text: string } | undefined>(undefined) const [busy, setBusy] = useState(false) const run = async (): Promise => { setBusy(true) setMessage(undefined) try { const made = (await call("documents.export.create", { ...slugs, fileId })) as { filename: string contentType: string body: string } const bytes = Uint8Array.from(atob(made.body), (char) => char.charCodeAt(0)) const url = URL.createObjectURL(new Blob([bytes], { type: made.contentType })) const link = document.createElement("a") link.href = url link.download = made.filename // Attached while it is clicked, and the object URL kept for a while after: a browser starts the // download after the click returns, and a URL revoked first is a download that never starts. document.body.append(link) link.click() link.remove() setTimeout(() => URL.revokeObjectURL(url), 60_000) setMessage({ ok: true, text: labels.exported }) } catch (error) { setMessage({ ok: false, text: labels.failed.replace("{detail}", describe(error)) }) } finally { setBusy(false) } } return (

{approved === 0 ? labels.nothing : labels.hint}

{message?.text ?? ""}

) }