feat(web): add missing error boundaries across all route groups
- Add global-error.tsx at app root (inline styles, wraps html/body) - Add group-level error.tsx for (public) — catches all unguarded public routes - Add per-route error.tsx for high-traffic public segments: listings, listings/[id], du-an, du-an/[slug], khu-cong-nghiep, khu-cong-nghiep/[slug], agents, agents/[id], payment - Add auth/callback/error.tsx for OAuth callback failures - Commit coverage table to apps/web/docs/error-boundary-coverage.md Pre-existing API test failures unrelated to this change (broker-cert, update-listing-status, mcp.module) were already failing on master. Closes GOO-115 Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
60
apps/web/app/[locale]/(public)/agents/[id]/error.tsx
Normal file
60
apps/web/app/[locale]/(public)/agents/[id]/error.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function AgentProfileError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error('Agent profile error:', error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 py-12">
|
||||
<div className="flex min-h-[400px] flex-col items-center justify-center">
|
||||
<div className="mx-auto max-w-md text-center">
|
||||
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-destructive/10">
|
||||
<svg
|
||||
className="h-7 w-7 text-destructive"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-4 text-xl font-semibold">Không thể tải hồ sơ môi giới</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Đã xảy ra lỗi khi tải thông tin môi giới. Vui lòng thử lại.
|
||||
</p>
|
||||
{error.digest && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">Mã lỗi: {error.digest}</p>
|
||||
)}
|
||||
<div className="mt-6 flex justify-center gap-3">
|
||||
<button
|
||||
onClick={reset}
|
||||
className="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90"
|
||||
>
|
||||
Thử lại
|
||||
</button>
|
||||
<a
|
||||
href="/agents"
|
||||
className="inline-flex h-9 items-center rounded-md border border-input bg-background px-4 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
Về danh sách môi giới
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
apps/web/app/[locale]/(public)/agents/error.tsx
Normal file
60
apps/web/app/[locale]/(public)/agents/error.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function AgentsError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error('Agents page error:', error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 py-12">
|
||||
<div className="flex min-h-[400px] flex-col items-center justify-center">
|
||||
<div className="mx-auto max-w-md text-center">
|
||||
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-destructive/10">
|
||||
<svg
|
||||
className="h-7 w-7 text-destructive"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-4 text-xl font-semibold">Không thể tải thông tin môi giới</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Đã xảy ra lỗi khi tải danh sách môi giới. Vui lòng thử lại.
|
||||
</p>
|
||||
{error.digest && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">Mã lỗi: {error.digest}</p>
|
||||
)}
|
||||
<div className="mt-6 flex justify-center gap-3">
|
||||
<button
|
||||
onClick={reset}
|
||||
className="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90"
|
||||
>
|
||||
Thử lại
|
||||
</button>
|
||||
<a
|
||||
href="/"
|
||||
className="inline-flex h-9 items-center rounded-md border border-input bg-background px-4 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
Về trang chủ
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
apps/web/app/[locale]/(public)/du-an/[slug]/error.tsx
Normal file
60
apps/web/app/[locale]/(public)/du-an/[slug]/error.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function ProjectDetailError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error('Project detail error:', error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 py-12">
|
||||
<div className="flex min-h-[400px] flex-col items-center justify-center">
|
||||
<div className="mx-auto max-w-md text-center">
|
||||
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-destructive/10">
|
||||
<svg
|
||||
className="h-7 w-7 text-destructive"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-4 text-xl font-semibold">Không thể tải thông tin dự án</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Đã xảy ra lỗi khi tải chi tiết dự án. Vui lòng thử lại.
|
||||
</p>
|
||||
{error.digest && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">Mã lỗi: {error.digest}</p>
|
||||
)}
|
||||
<div className="mt-6 flex justify-center gap-3">
|
||||
<button
|
||||
onClick={reset}
|
||||
className="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90"
|
||||
>
|
||||
Thử lại
|
||||
</button>
|
||||
<a
|
||||
href="/du-an"
|
||||
className="inline-flex h-9 items-center rounded-md border border-input bg-background px-4 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
Về danh sách dự án
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
apps/web/app/[locale]/(public)/du-an/error.tsx
Normal file
60
apps/web/app/[locale]/(public)/du-an/error.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function ProjectsError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error('Projects (du-an) error:', error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 py-12">
|
||||
<div className="flex min-h-[400px] flex-col items-center justify-center">
|
||||
<div className="mx-auto max-w-md text-center">
|
||||
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-destructive/10">
|
||||
<svg
|
||||
className="h-7 w-7 text-destructive"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-4 text-xl font-semibold">Không thể tải danh sách dự án</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Đã xảy ra lỗi khi tải dự án bất động sản. Vui lòng thử lại.
|
||||
</p>
|
||||
{error.digest && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">Mã lỗi: {error.digest}</p>
|
||||
)}
|
||||
<div className="mt-6 flex justify-center gap-3">
|
||||
<button
|
||||
onClick={reset}
|
||||
className="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90"
|
||||
>
|
||||
Thử lại
|
||||
</button>
|
||||
<a
|
||||
href="/"
|
||||
className="inline-flex h-9 items-center rounded-md border border-input bg-background px-4 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
Về trang chủ
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
apps/web/app/[locale]/(public)/error.tsx
Normal file
60
apps/web/app/[locale]/(public)/error.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function PublicError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error('Public page error:', error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 py-12">
|
||||
<div className="flex min-h-[400px] flex-col items-center justify-center">
|
||||
<div className="mx-auto max-w-md text-center">
|
||||
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-destructive/10">
|
||||
<svg
|
||||
className="h-7 w-7 text-destructive"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
<h2 className="mt-4 text-xl font-semibold">Không thể tải trang</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Đã xảy ra lỗi khi tải nội dung. Vui lòng thử lại.
|
||||
</p>
|
||||
{error.digest && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">Mã lỗi: {error.digest}</p>
|
||||
)}
|
||||
<div className="mt-6 flex justify-center gap-3">
|
||||
<button
|
||||
onClick={reset}
|
||||
className="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90"
|
||||
>
|
||||
Thử lại
|
||||
</button>
|
||||
<a
|
||||
href="/"
|
||||
className="inline-flex h-9 items-center rounded-md border border-input bg-background px-4 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
Về trang chủ
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function IndustrialParkDetailError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error('Industrial park detail error:', error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 py-12">
|
||||
<div className="flex min-h-[400px] flex-col items-center justify-center">
|
||||
<div className="mx-auto max-w-md text-center">
|
||||
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-destructive/10">
|
||||
<svg
|
||||
className="h-7 w-7 text-destructive"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-4 text-xl font-semibold">Không thể tải chi tiết khu công nghiệp</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Đã xảy ra lỗi khi tải thông tin khu công nghiệp. Vui lòng thử lại.
|
||||
</p>
|
||||
{error.digest && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">Mã lỗi: {error.digest}</p>
|
||||
)}
|
||||
<div className="mt-6 flex justify-center gap-3">
|
||||
<button
|
||||
onClick={reset}
|
||||
className="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90"
|
||||
>
|
||||
Thử lại
|
||||
</button>
|
||||
<a
|
||||
href="/khu-cong-nghiep"
|
||||
className="inline-flex h-9 items-center rounded-md border border-input bg-background px-4 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
Về danh sách khu công nghiệp
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
apps/web/app/[locale]/(public)/khu-cong-nghiep/error.tsx
Normal file
60
apps/web/app/[locale]/(public)/khu-cong-nghiep/error.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function IndustrialParksError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error('Industrial parks error:', error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 py-12">
|
||||
<div className="flex min-h-[400px] flex-col items-center justify-center">
|
||||
<div className="mx-auto max-w-md text-center">
|
||||
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-destructive/10">
|
||||
<svg
|
||||
className="h-7 w-7 text-destructive"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-4 text-xl font-semibold">Không thể tải thông tin khu công nghiệp</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Đã xảy ra lỗi khi tải dữ liệu khu công nghiệp. Vui lòng thử lại.
|
||||
</p>
|
||||
{error.digest && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">Mã lỗi: {error.digest}</p>
|
||||
)}
|
||||
<div className="mt-6 flex justify-center gap-3">
|
||||
<button
|
||||
onClick={reset}
|
||||
className="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90"
|
||||
>
|
||||
Thử lại
|
||||
</button>
|
||||
<a
|
||||
href="/"
|
||||
className="inline-flex h-9 items-center rounded-md border border-input bg-background px-4 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
Về trang chủ
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
apps/web/app/[locale]/(public)/listings/[id]/error.tsx
Normal file
60
apps/web/app/[locale]/(public)/listings/[id]/error.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function ListingDetailError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error('Listing detail error:', error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 py-12">
|
||||
<div className="flex min-h-[400px] flex-col items-center justify-center">
|
||||
<div className="mx-auto max-w-md text-center">
|
||||
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-destructive/10">
|
||||
<svg
|
||||
className="h-7 w-7 text-destructive"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-4 text-xl font-semibold">Không thể tải thông tin bất động sản</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Đã xảy ra lỗi khi tải chi tiết bất động sản. Vui lòng thử lại.
|
||||
</p>
|
||||
{error.digest && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">Mã lỗi: {error.digest}</p>
|
||||
)}
|
||||
<div className="mt-6 flex justify-center gap-3">
|
||||
<button
|
||||
onClick={reset}
|
||||
className="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90"
|
||||
>
|
||||
Thử lại
|
||||
</button>
|
||||
<a
|
||||
href="/listings"
|
||||
className="inline-flex h-9 items-center rounded-md border border-input bg-background px-4 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
Về danh sách
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
apps/web/app/[locale]/(public)/listings/error.tsx
Normal file
60
apps/web/app/[locale]/(public)/listings/error.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function ListingsError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error('Listings error:', error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 py-12">
|
||||
<div className="flex min-h-[400px] flex-col items-center justify-center">
|
||||
<div className="mx-auto max-w-md text-center">
|
||||
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-destructive/10">
|
||||
<svg
|
||||
className="h-7 w-7 text-destructive"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-4 text-xl font-semibold">Không thể tải danh sách bất động sản</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Đã xảy ra lỗi khi tải danh sách. Vui lòng thử lại.
|
||||
</p>
|
||||
{error.digest && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">Mã lỗi: {error.digest}</p>
|
||||
)}
|
||||
<div className="mt-6 flex justify-center gap-3">
|
||||
<button
|
||||
onClick={reset}
|
||||
className="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90"
|
||||
>
|
||||
Thử lại
|
||||
</button>
|
||||
<a
|
||||
href="/"
|
||||
className="inline-flex h-9 items-center rounded-md border border-input bg-background px-4 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
Về trang chủ
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
apps/web/app/[locale]/(public)/payment/error.tsx
Normal file
60
apps/web/app/[locale]/(public)/payment/error.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function PaymentError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error('Payment page error:', error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 py-12">
|
||||
<div className="flex min-h-[400px] flex-col items-center justify-center">
|
||||
<div className="mx-auto max-w-md text-center">
|
||||
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-destructive/10">
|
||||
<svg
|
||||
className="h-7 w-7 text-destructive"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-4 text-xl font-semibold">Lỗi thanh toán</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Đã xảy ra lỗi trong quá trình thanh toán. Vui lòng thử lại hoặc liên hệ hỗ trợ.
|
||||
</p>
|
||||
{error.digest && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">Mã lỗi: {error.digest}</p>
|
||||
)}
|
||||
<div className="mt-6 flex justify-center gap-3">
|
||||
<button
|
||||
onClick={reset}
|
||||
className="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90"
|
||||
>
|
||||
Thử lại
|
||||
</button>
|
||||
<a
|
||||
href="/dashboard"
|
||||
className="inline-flex h-9 items-center rounded-md border border-input bg-background px-4 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
Về bảng điều khiển
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
58
apps/web/app/[locale]/auth/callback/error.tsx
Normal file
58
apps/web/app/[locale]/auth/callback/error.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function AuthCallbackError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error('Auth callback error:', error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center px-4">
|
||||
<div className="mx-auto max-w-md text-center">
|
||||
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-destructive/10">
|
||||
<svg
|
||||
className="h-7 w-7 text-destructive"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-4 text-xl font-semibold">Lỗi đăng nhập</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Không thể hoàn tất quá trình đăng nhập. Vui lòng thử lại.
|
||||
</p>
|
||||
{error.digest && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">Mã lỗi: {error.digest}</p>
|
||||
)}
|
||||
<div className="mt-6 flex justify-center gap-3">
|
||||
<button
|
||||
onClick={reset}
|
||||
className="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90"
|
||||
>
|
||||
Thử lại
|
||||
</button>
|
||||
<a
|
||||
href="/login"
|
||||
className="inline-flex h-9 items-center rounded-md border border-input bg-background px-4 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
Về trang đăng nhập
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
126
apps/web/app/global-error.tsx
Normal file
126
apps/web/app/global-error.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function GlobalError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error('Global error:', error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<html lang="vi">
|
||||
<body>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
minHeight: '100vh',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '1rem',
|
||||
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||
backgroundColor: '#f9fafb',
|
||||
}}
|
||||
>
|
||||
<div style={{ maxWidth: '28rem', textAlign: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
margin: '0 auto',
|
||||
display: 'flex',
|
||||
height: '3.5rem',
|
||||
width: '3.5rem',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '9999px',
|
||||
backgroundColor: '#fee2e2',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
style={{ height: '1.75rem', width: '1.75rem', color: '#ef4444' }}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
<h1
|
||||
style={{
|
||||
marginTop: '1rem',
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 600,
|
||||
color: '#111827',
|
||||
}}
|
||||
>
|
||||
Đã xảy ra lỗi nghiêm trọng
|
||||
</h1>
|
||||
<p style={{ marginTop: '0.5rem', fontSize: '0.875rem', color: '#6b7280' }}>
|
||||
Ứng dụng gặp sự cố không mong muốn. Vui lòng tải lại trang.
|
||||
</p>
|
||||
{error.digest && (
|
||||
<p style={{ marginTop: '0.25rem', fontSize: '0.75rem', color: '#9ca3af' }}>
|
||||
Mã lỗi: {error.digest}
|
||||
</p>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
marginTop: '1.5rem',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
gap: '0.75rem',
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={reset}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
height: '2.25rem',
|
||||
alignItems: 'center',
|
||||
borderRadius: '0.375rem',
|
||||
backgroundColor: '#2563eb',
|
||||
padding: '0 1rem',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
color: '#ffffff',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Thử lại
|
||||
</button>
|
||||
<a
|
||||
href="/"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
height: '2.25rem',
|
||||
alignItems: 'center',
|
||||
borderRadius: '0.375rem',
|
||||
border: '1px solid #d1d5db',
|
||||
backgroundColor: '#ffffff',
|
||||
padding: '0 1rem',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
color: '#374151',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
Về trang chủ
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
93
apps/web/docs/error-boundary-coverage.md
Normal file
93
apps/web/docs/error-boundary-coverage.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Error Boundary Coverage
|
||||
|
||||
Audited: 2026-04-24 | Issue: [GOO-115](/GOO/issues/GOO-115)
|
||||
|
||||
## Summary
|
||||
|
||||
| Route group | Segment | `error.tsx` | Notes |
|
||||
|---|---|:---:|---|
|
||||
| root | `app/` | ✅ | `app/error.tsx` |
|
||||
| root global | `app/` | ✅ | `app/global-error.tsx` — added GOO-115 |
|
||||
| locale root | `app/[locale]/` | ✅ | `app/[locale]/error.tsx` |
|
||||
| **(admin)** group | `(admin)/` | ✅ | covers all admin sub-routes |
|
||||
| **(auth)** group | `(auth)/` | ✅ | covers login / register |
|
||||
| auth callback | `[locale]/auth/callback/` | ✅ | added GOO-115 |
|
||||
| **(dashboard)** group | `(dashboard)/` | ✅ | covers all dashboard sub-routes |
|
||||
| **(public)** group | `(public)/` | ✅ | added GOO-115 — fallback for uncovered public routes |
|
||||
| public — search | `(public)/search/` | ✅ | existed pre-audit |
|
||||
| public — listings | `(public)/listings/` | ✅ | added GOO-115 |
|
||||
| public — listings detail | `(public)/listings/[id]/` | ✅ | added GOO-115 |
|
||||
| public — du-an | `(public)/du-an/` | ✅ | added GOO-115 |
|
||||
| public — du-an detail | `(public)/du-an/[slug]/` | ✅ | added GOO-115 |
|
||||
| public — khu-cong-nghiep | `(public)/khu-cong-nghiep/` | ✅ | added GOO-115 |
|
||||
| public — khu-cong-nghiep detail | `(public)/khu-cong-nghiep/[slug]/` | ✅ | added GOO-115 |
|
||||
| public — agents | `(public)/agents/` | ✅ | added GOO-115 |
|
||||
| public — agent profile | `(public)/agents/[id]/` | ✅ | added GOO-115 |
|
||||
| public — payment | `(public)/payment/` | ✅ | added GOO-115 |
|
||||
|
||||
## Routes covered by group boundary (no per-route file needed)
|
||||
|
||||
These routes fall under a group-level `error.tsx` that handles them:
|
||||
|
||||
| Route | Covered by |
|
||||
|---|---|
|
||||
| `(public)/bao-cao/` | `(public)/error.tsx` |
|
||||
| `(public)/bao-cao/[id]/` | `(public)/error.tsx` |
|
||||
| `(public)/bao-cao/tao-moi/` | `(public)/error.tsx` |
|
||||
| `(public)/chuyen-nhuong/` | `(public)/error.tsx` |
|
||||
| `(public)/chuyen-nhuong/[id]/` | `(public)/error.tsx` |
|
||||
| `(public)/chuyen-nhuong/dang-tin/` | `(public)/error.tsx` |
|
||||
| `(public)/compare/` | `(public)/error.tsx` |
|
||||
| `(public)/design-system/` | `(public)/error.tsx` |
|
||||
| `(public)/khu-cong-nghiep/cho-thue/` | `(public)/khu-cong-nghiep/error.tsx` |
|
||||
| `(public)/khu-cong-nghiep/so-sanh/` | `(public)/khu-cong-nghiep/error.tsx` |
|
||||
| `(public)/payment/return/` | `(public)/payment/error.tsx` |
|
||||
| `(public)/pricing/` | `(public)/error.tsx` |
|
||||
| `(admin)/admin/accounts/developers/` | `(admin)/error.tsx` |
|
||||
| `(admin)/admin/accounts/park-operators/` | `(admin)/error.tsx` |
|
||||
| `(admin)/admin/audit-log/` | `(admin)/error.tsx` |
|
||||
| `(admin)/admin/kyc/` | `(admin)/error.tsx` |
|
||||
| `(admin)/admin/moderation/` | `(admin)/error.tsx` |
|
||||
| `(admin)/admin/settings/ai/` | `(admin)/error.tsx` |
|
||||
| `(admin)/admin/users/` | `(admin)/error.tsx` |
|
||||
| `(auth)/login/` | `(auth)/error.tsx` |
|
||||
| `(auth)/register/` | `(auth)/error.tsx` |
|
||||
| `(dashboard)/dashboard/kyc/` | `(dashboard)/error.tsx` |
|
||||
| `(dashboard)/dashboard/payments/` | `(dashboard)/error.tsx` |
|
||||
| `(dashboard)/dashboard/profile/` | `(dashboard)/error.tsx` |
|
||||
| `(dashboard)/dashboard/reports/` | `(dashboard)/error.tsx` |
|
||||
| `(dashboard)/dashboard/reports/[id]/` | `(dashboard)/error.tsx` |
|
||||
| `(dashboard)/dashboard/reports/new/` | `(dashboard)/error.tsx` |
|
||||
| `(dashboard)/dashboard/saved-searches/` | `(dashboard)/error.tsx` |
|
||||
| `(dashboard)/dashboard/subscription/` | `(dashboard)/error.tsx` |
|
||||
| `(dashboard)/dashboard/valuation/` | `(dashboard)/error.tsx` |
|
||||
| `(dashboard)/dev/tokens/` | `(dashboard)/error.tsx` |
|
||||
| `(dashboard)/industrial-parks/` | `(dashboard)/error.tsx` |
|
||||
| `(dashboard)/industrial-parks/[id]/edit/` | `(dashboard)/error.tsx` |
|
||||
| `(dashboard)/industrial-parks/new/` | `(dashboard)/error.tsx` |
|
||||
| `(dashboard)/inquiries/` | `(dashboard)/error.tsx` |
|
||||
| `(dashboard)/leads/` | `(dashboard)/error.tsx` |
|
||||
| `(dashboard)/my-listings/` | `(dashboard)/error.tsx` |
|
||||
| `(dashboard)/my-listings/[id]/edit/` | `(dashboard)/error.tsx` |
|
||||
| `(dashboard)/my-listings/new/` | `(dashboard)/error.tsx` |
|
||||
| `(dashboard)/projects/` | `(dashboard)/error.tsx` |
|
||||
| `(dashboard)/projects/[id]/edit/` | `(dashboard)/error.tsx` |
|
||||
| `(dashboard)/projects/new/` | `(dashboard)/error.tsx` |
|
||||
| `(dashboard)/analytics/` | `(dashboard)/error.tsx` |
|
||||
| `[locale]/auth/callback/google/` | `auth/callback/error.tsx` |
|
||||
| `[locale]/auth/callback/zalo/` | `auth/callback/error.tsx` |
|
||||
|
||||
## Files added in GOO-115
|
||||
|
||||
- `apps/web/app/global-error.tsx`
|
||||
- `apps/web/app/[locale]/(public)/error.tsx`
|
||||
- `apps/web/app/[locale]/(public)/listings/error.tsx`
|
||||
- `apps/web/app/[locale]/(public)/listings/[id]/error.tsx`
|
||||
- `apps/web/app/[locale]/(public)/du-an/error.tsx`
|
||||
- `apps/web/app/[locale]/(public)/du-an/[slug]/error.tsx`
|
||||
- `apps/web/app/[locale]/(public)/khu-cong-nghiep/error.tsx`
|
||||
- `apps/web/app/[locale]/(public)/khu-cong-nghiep/[slug]/error.tsx`
|
||||
- `apps/web/app/[locale]/(public)/agents/error.tsx`
|
||||
- `apps/web/app/[locale]/(public)/agents/[id]/error.tsx`
|
||||
- `apps/web/app/[locale]/(public)/payment/error.tsx`
|
||||
- `apps/web/app/[locale]/auth/callback/error.tsx`
|
||||
Reference in New Issue
Block a user