feat(web): add ErrorBoundary, PageErrorBoundary, ComponentErrorBoundary
Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 4s
CI / E2E Tests (push) Has been skipped
CI / AI Services (Python) — Smoke (push) Failing after 6s
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 35s
Deploy / Build API Image (push) Failing after 15s
Deploy / Build Web Image (push) Failing after 13s
Deploy / Build AI Services Image (push) Failing after 11s
E2E Tests / Playwright E2E (push) Failing after 11s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 3s
Security Scanning / Trivy Scan — API Image (push) Failing after 1m33s
Security Scanning / Trivy Scan — Web Image (push) Failing after 54s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 45s
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Security Scanning / Trivy Filesystem Scan (push) Failing after 46s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Security Scanning / Security Gate (push) Failing after 1s
Deploy / Rollback Staging (push) Has been skipped
Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 4s
CI / E2E Tests (push) Has been skipped
CI / AI Services (Python) — Smoke (push) Failing after 6s
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 35s
Deploy / Build API Image (push) Failing after 15s
Deploy / Build Web Image (push) Failing after 13s
Deploy / Build AI Services Image (push) Failing after 11s
E2E Tests / Playwright E2E (push) Failing after 11s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 3s
Security Scanning / Trivy Scan — API Image (push) Failing after 1m33s
Security Scanning / Trivy Scan — Web Image (push) Failing after 54s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 45s
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Security Scanning / Trivy Filesystem Scan (push) Failing after 46s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Security Scanning / Security Gate (push) Failing after 1s
Deploy / Rollback Staging (push) Has been skipped
Implements GOO-63 audit requirement — React error boundaries with Vietnamese-language fallback UI, Sentry capture, and "Thử lại" retry. - ErrorBoundary: generic class component wrapping Sentry.captureException - PageErrorBoundary: full-page fallback for route layouts - ComponentErrorBoundary: inline widget fallback (compact + standard modes) - Applied to ListingMap, CheckoutModal, SearchResults as first targets Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
120
apps/web/components/error-boundary/component-error-boundary.tsx
Normal file
120
apps/web/components/error-boundary/component-error-boundary.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
'use client';
|
||||
|
||||
import { type ErrorInfo, type ReactNode } from 'react';
|
||||
import { ErrorBoundary, type ErrorBoundaryFallbackProps } from './error-boundary';
|
||||
|
||||
interface ComponentErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
/**
|
||||
* Short label identifying the widget (e.g. "bản đồ", "thanh toán", "tìm kiếm").
|
||||
* Shown in the fallback UI so users understand which section failed.
|
||||
*/
|
||||
label?: string;
|
||||
/**
|
||||
* Compact mode renders a smaller fallback suited to narrow widgets like cards
|
||||
* or sidebars. Defaults to false.
|
||||
*/
|
||||
compact?: boolean;
|
||||
onError?: (error: Error, info: ErrorInfo) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline error boundary for critical widgets (map, payment form, search).
|
||||
*
|
||||
* Renders a contained, non-intrusive fallback that does not take over the page.
|
||||
* Captures errors to Sentry via the base `ErrorBoundary`.
|
||||
*/
|
||||
export function ComponentErrorBoundary({
|
||||
children,
|
||||
label,
|
||||
compact = false,
|
||||
onError,
|
||||
}: ComponentErrorBoundaryProps) {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
onError={onError}
|
||||
fallback={({ error, reset }) => (
|
||||
<ComponentFallback error={error} reset={reset} label={label} compact={compact} />
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function ComponentFallback({
|
||||
error,
|
||||
reset,
|
||||
label,
|
||||
compact,
|
||||
}: ErrorBoundaryFallbackProps & { label?: string; compact: boolean }) {
|
||||
if (compact) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-3 rounded-md border border-destructive/20 bg-destructive/5 px-3 py-2 text-sm"
|
||||
role="alert"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4 shrink-0 text-destructive"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="flex-1 text-xs text-muted-foreground">
|
||||
{label ? `Lỗi ${label}` : 'Đã xảy ra lỗi'}
|
||||
{process.env.NODE_ENV !== 'production' && error.message
|
||||
? `: ${error.message}`
|
||||
: ''}
|
||||
</span>
|
||||
<button
|
||||
onClick={reset}
|
||||
className="shrink-0 text-xs font-medium text-primary underline-offset-2 hover:underline focus-visible:outline-none"
|
||||
>
|
||||
Thử lại
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex min-h-[160px] flex-col items-center justify-center rounded-lg border border-destructive/20 bg-destructive/5 px-6 py-6 text-center"
|
||||
role="alert"
|
||||
>
|
||||
<svg
|
||||
className="mb-2 h-7 w-7 text-destructive"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{label ? `Không thể tải ${label}` : 'Đã xảy ra lỗi'}
|
||||
</p>
|
||||
{process.env.NODE_ENV !== 'production' && error.message && (
|
||||
<p className="mt-1 max-w-xs truncate text-xs text-muted-foreground">{error.message}</p>
|
||||
)}
|
||||
<button
|
||||
onClick={reset}
|
||||
className="mt-4 inline-flex h-8 items-center justify-center rounded-md bg-primary px-4 text-xs font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
Thử lại
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user