fix+upstream: hooks-order crash, ConfirmDialog/PageHeader/skeletons, styled error boundary
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>
This commit is contained in:
73
app/lib/errors.ts
Normal file
73
app/lib/errors.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// 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"
|
||||
}
|
||||
Reference in New Issue
Block a user