Hoists hooks above early return (render-time Navigate); ports finance's ConfirmDialog/PageHeader/loading/states + APC error-copy mapper; removes window.confirm, dead appbar search, and seeded fake notifications; wires toasts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
74 lines
3.0 KiB
TypeScript
74 lines
3.0 KiB
TypeScript
// Human error copy. Maps error codes / shapes / thrown values to a single
|
|
// plain sentence a person can act on. Used by the root ErrorBoundary and
|
|
// available to any route that catches a failure (e.g. a fetch reject).
|
|
//
|
|
// Ported/generalised from arcadia-personal-cloud-web's `acceptErrorMessage`:
|
|
// switch on a known machine `code`, else flatten field-validation errors,
|
|
// else fall back to the raw message or a friendly default.
|
|
|
|
import { isRouteErrorResponse } from "react-router"
|
|
|
|
/** A known machine error code → the human sentence we show for it. Extend
|
|
* this map as the app grows real backend error codes. */
|
|
const CODE_MESSAGES: Record<string, string> = {
|
|
network_error: "Couldn't reach the server. Check your connection and try again.",
|
|
timeout: "That took too long to respond. Try again in a moment.",
|
|
unauthorized: "Your session has expired. Sign in again to continue.",
|
|
forbidden: "You don't have access to that.",
|
|
not_found: "We couldn't find what you were looking for.",
|
|
rate_limited: "Too many requests just now. Give it a few seconds.",
|
|
server_error: "Something went wrong on our end. Try again shortly.",
|
|
}
|
|
|
|
type ErrorShape = {
|
|
error?: string
|
|
code?: string
|
|
message?: string
|
|
errors?: Record<string, string[] | string>
|
|
}
|
|
|
|
/** Turn any caught value into one human sentence. */
|
|
export function humanErrorMessage(err: unknown): string {
|
|
// React Router route-error responses (thrown Responses / 404s etc.).
|
|
if (isRouteErrorResponse(err)) {
|
|
if (err.status === 404) return CODE_MESSAGES.not_found
|
|
if (err.status === 401) return CODE_MESSAGES.unauthorized
|
|
if (err.status === 403) return CODE_MESSAGES.forbidden
|
|
if (err.status === 429) return CODE_MESSAGES.rate_limited
|
|
if (err.status >= 500) return CODE_MESSAGES.server_error
|
|
return err.statusText || "That request couldn't be completed."
|
|
}
|
|
|
|
const d = (
|
|
err && typeof err === "object" ? err : {}
|
|
) as ErrorShape
|
|
const code = d.code ?? d.error
|
|
if (code && CODE_MESSAGES[code]) return CODE_MESSAGES[code]
|
|
|
|
// arcadia-style field validation: { errors: { field: [msg...] } } —
|
|
// flatten into a single readable line so the user knows what to fix.
|
|
if (d.errors && typeof d.errors === "object") {
|
|
const lines: string[] = []
|
|
for (const [field, msgs] of Object.entries(d.errors)) {
|
|
const label = field.replace(/_/g, " ")
|
|
const arr = Array.isArray(msgs) ? msgs : [String(msgs)]
|
|
for (const m of arr) lines.push(`${label} ${m}`)
|
|
}
|
|
if (lines.length) return lines.join("; ")
|
|
}
|
|
|
|
if (err instanceof Error && err.message) return err.message
|
|
if (typeof d.message === "string" && d.message) return d.message
|
|
|
|
return "An unexpected error occurred. Please try again."
|
|
}
|
|
|
|
/** Short heading to pair with {@link humanErrorMessage} in a boundary/card. */
|
|
export function humanErrorTitle(err: unknown): string {
|
|
if (isRouteErrorResponse(err)) {
|
|
if (err.status === 404) return "Page not found"
|
|
return "Something went wrong"
|
|
}
|
|
return "Something went wrong"
|
|
}
|