fix issue

This commit is contained in:
Marshal
2026-07-16 01:05:57 +00:00
parent 41fe04652f
commit fe29b38377
18 changed files with 518 additions and 51 deletions

View File

@@ -0,0 +1,143 @@
import { useEffect, useState } from "react";
import { Alert, Button, Group, List, Modal, Stack, Text } from "@mantine/core";
import { AlertTriangle } from "lucide-react";
/**
* Global API error modal.
*
* The axios client emits every failed request (with a server response) through
* `emitApiError`; this modal — mounted once at the app root — shows the
* SERVER'S actual `message` instead of a generic "Request failed with status
* code NNN". Pages listed in EXCLUDED_PATH_PATTERNS (warehouse, first/last
* mile, onboarding/auth screens) keep their own inline error handling and
* never trigger it.
*/
export interface ApiErrorPayload {
/** Server messages — the `message` field (string or class-validator array). */
messages: string[];
statusCode?: number;
/** API path that failed, shown as small helper text. */
path?: string;
}
type Listener = (payload: ApiErrorPayload) => void;
let listener: Listener | null = null;
/** Current-page path patterns where the global modal must stay silent. */
const EXCLUDED_PATH_PATTERNS = [
/^\/auth/,
/^\/callback/,
/warehouse/i,
/first-mile/i,
/last-mile/i,
/onboard/i,
/register/i,
];
export function isGlobalErrorModalSuppressed(pathname: string): boolean {
return EXCLUDED_PATH_PATTERNS.some((re) => re.test(pathname));
}
export function emitApiError(payload: ApiErrorPayload): void {
if (isGlobalErrorModalSuppressed(window.location.pathname)) return;
listener?.(payload);
}
/** Pull the server `message` out of an axios-style error. */
export function extractApiErrorPayload(error: unknown): ApiErrorPayload | null {
const err = error as {
response?: {
status?: number;
data?: {
message?: string | string[];
error?: string;
statusCode?: number;
path?: string;
};
};
};
const response = err?.response;
if (!response) return null; // network error / cancellation — not ours
const data = response.data;
const raw = data?.message;
const messages = Array.isArray(raw)
? raw.filter((m): m is string => typeof m === "string" && m.length > 0)
: typeof raw === "string" && raw.length > 0
? [raw]
: [];
if (messages.length === 0) {
messages.push(
data?.error ?? `Request failed with status code ${response.status}`,
);
}
return {
messages,
statusCode: data?.statusCode ?? response.status,
path: data?.path,
};
}
export function ApiErrorModal() {
const [payload, setPayload] = useState<ApiErrorPayload | null>(null);
useEffect(() => {
listener = (next) => {
// Don't stack identical messages while the modal is already showing them.
setPayload((current) =>
current && current.messages.join("\n") === next.messages.join("\n")
? current
: next,
);
};
return () => {
listener = null;
};
}, []);
const close = () => setPayload(null);
return (
<Modal
opened={payload !== null}
onClose={close}
centered
radius="md"
title={
<Group gap="xs">
<AlertTriangle size={18} color="var(--mantine-color-red-6)" />
<Text fw={700}>Request failed</Text>
</Group>
}
>
{payload && (
<Stack gap="md">
<Alert color="red" variant="light">
{payload.messages.length === 1 ? (
<Text size="sm">{payload.messages[0]}</Text>
) : (
<List size="sm" spacing={4}>
{payload.messages.map((m, i) => (
<List.Item key={i}>{m}</List.Item>
))}
</List>
)}
</Alert>
{(payload.statusCode || payload.path) && (
<Text size="xs" c="dimmed">
{payload.statusCode ? `Status ${payload.statusCode}` : null}
{payload.statusCode && payload.path ? " · " : null}
{payload.path}
</Text>
)}
<Group justify="flex-end">
<Button variant="default" onClick={close}>
Close
</Button>
</Group>
</Stack>
)}
</Modal>
);
}