Skip to content

Commit 76e7ba5

Browse files
committed
feat: add global error boundary
1 parent aabd9e8 commit 76e7ba5

File tree

1 file changed

+248
-0
lines changed

1 file changed

+248
-0
lines changed

app/global-error.tsx

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
"use client";
2+
3+
import { Button } from "@/components/ui/button";
4+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
5+
import { Badge } from "@/components/ui/badge";
6+
import {
7+
ERROR_NOT_FOUND,
8+
ERROR_UNAUTHORIZED,
9+
ERROR_USER_VERIFIED,
10+
ERROR_NOT_IMPLEMENTED
11+
} from "@/lib/apollo";
12+
import { ApolloError } from "@apollo/client";
13+
import {
14+
AlertCircle,
15+
RefreshCw,
16+
Home,
17+
Lock,
18+
Search,
19+
WifiOff,
20+
Code,
21+
Shield
22+
} from "lucide-react";
23+
import Link from "next/link";
24+
import { Logo } from "@/components/logo";
25+
import { useEffect } from "react";
26+
27+
interface GlobalErrorProps {
28+
error: (Error & { digest?: string }) | ApolloError;
29+
reset: () => void;
30+
}
31+
32+
function getErrorInfo(error: Error | ApolloError) {
33+
// Check if it's an ApolloError
34+
if (error instanceof ApolloError) {
35+
// Network errors
36+
if (error.networkError && 'statusCode' in error.networkError) {
37+
switch (error.networkError.statusCode) {
38+
case 401:
39+
return {
40+
title: "未經授權",
41+
description: "您的登入狀態已過期,請重新登入。",
42+
icon: Lock,
43+
actionHref: "/login"
44+
};
45+
case 403:
46+
return {
47+
title: "權限不足",
48+
description: "您沒有權限執行此操作。",
49+
icon: Shield
50+
};
51+
case 404:
52+
return {
53+
title: "找不到資源",
54+
description: "請求的資源不存在或已被移除。",
55+
icon: Search
56+
};
57+
case 500:
58+
return {
59+
title: "伺服器錯誤",
60+
description: "伺服器發生內部錯誤,請稍後再試。",
61+
icon: AlertCircle
62+
};
63+
}
64+
}
65+
66+
if (error.networkError) {
67+
return {
68+
title: "網路連線錯誤",
69+
description: "無法連接到伺服器,請檢查網路連線。",
70+
icon: WifiOff
71+
};
72+
}
73+
74+
// GraphQL errors with codes
75+
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
76+
const firstError = error.graphQLErrors[0];
77+
const errorCode = firstError.extensions?.code as string;
78+
79+
switch (errorCode) {
80+
case ERROR_NOT_FOUND:
81+
return {
82+
title: "找不到資料",
83+
description: "請求的資料不存在或已被刪除。",
84+
icon: Search
85+
};
86+
case ERROR_UNAUTHORIZED:
87+
return {
88+
title: "未經授權",
89+
description: "請登入後再試,或您的權限不足。",
90+
icon: Lock,
91+
actionHref: "/login"
92+
};
93+
case ERROR_USER_VERIFIED:
94+
return {
95+
title: "帳號已驗證",
96+
description: "此帳號已經完成驗證程序。",
97+
icon: Shield
98+
};
99+
case ERROR_NOT_IMPLEMENTED:
100+
return {
101+
title: "功能未實作",
102+
description: "此功能目前尚未實作,請稍後再試。",
103+
icon: Code
104+
};
105+
}
106+
107+
return {
108+
title: "GraphQL 查詢錯誤",
109+
description: firstError.message || "GraphQL 查詢發生錯誤。",
110+
icon: AlertCircle
111+
};
112+
}
113+
}
114+
115+
// Regular JavaScript errors
116+
return {
117+
title: "應用程式發生錯誤",
118+
description: error.message || "應用程式遇到預期外的錯誤。",
119+
icon: AlertCircle
120+
};
121+
}
122+
123+
export default function GlobalError({ error, reset }: GlobalErrorProps) {
124+
const errorInfo = getErrorInfo(error);
125+
126+
useEffect(() => {
127+
// Log error to monitoring service
128+
console.error("Global error:", error);
129+
}, [error]);
130+
131+
return (
132+
<html>
133+
<body>
134+
<div
135+
className={`
136+
flex min-h-svh flex-col items-center justify-center gap-6
137+
bg-gradient-to-br from-red-50 via-white to-red-100 p-6
138+
md:p-10
139+
`}
140+
>
141+
<Link
142+
href="/"
143+
className={`flex items-center gap-2 self-center font-medium`}
144+
>
145+
<div
146+
className={`
147+
flex size-6 items-center justify-center rounded-md
148+
text-primary-foreground
149+
`}
150+
>
151+
<Logo />
152+
</div>
153+
Database Playground
154+
</Link>
155+
156+
<Card className="min-w-md max-w-2xl">
157+
<CardHeader className="flex w-full flex-col items-center text-center">
158+
<errorInfo.icon className="mb-2 size-7 text-red-500" />
159+
<CardTitle className="text-xl">{errorInfo.title}</CardTitle>
160+
<CardDescription>
161+
{errorInfo.description}
162+
</CardDescription>
163+
</CardHeader>
164+
165+
<CardContent className="flex flex-col items-center gap-4">
166+
<div className="w-full rounded-md bg-red-50 p-4 text-left">
167+
<details className="text-sm">
168+
<summary className="cursor-pointer font-medium text-red-800">
169+
錯誤詳細資訊
170+
</summary>
171+
<div className="text-red-700 space-y-2 mt-2">
172+
<p className="font-medium">{error.name}: {error.message}</p>
173+
174+
{error.stack && (
175+
<pre className="whitespace-pre-wrap text-xs bg-red-100 p-2 rounded overflow-x-auto">
176+
{error.stack}
177+
</pre>
178+
)}
179+
180+
{error instanceof ApolloError && (
181+
<div className="space-y-2">
182+
{error.networkError && (
183+
<div>
184+
<Badge variant="destructive" className="text-xs mb-1">Network Error</Badge>
185+
<pre className="whitespace-pre-wrap text-xs bg-red-100 p-2 rounded">
186+
{JSON.stringify(error.networkError, null, 2)}
187+
</pre>
188+
</div>
189+
)}
190+
191+
{error.graphQLErrors && error.graphQLErrors.length > 0 && (
192+
<div>
193+
<Badge variant="destructive" className="text-xs mb-1">
194+
GraphQL Errors ({error.graphQLErrors.length})
195+
</Badge>
196+
<pre className="whitespace-pre-wrap text-xs bg-red-100 p-2 rounded">
197+
{JSON.stringify(error.graphQLErrors, null, 2)}
198+
</pre>
199+
</div>
200+
)}
201+
</div>
202+
)}
203+
</div>
204+
</details>
205+
</div>
206+
207+
<div className="flex flex-col sm:flex-row gap-3">
208+
<Button onClick={reset} variant="default" className="flex items-center gap-2">
209+
<RefreshCw className="size-4" />
210+
重試
211+
</Button>
212+
213+
{errorInfo.actionHref ? (
214+
<Button asChild variant="outline" className="flex items-center gap-2">
215+
<Link href={errorInfo.actionHref}>
216+
前往處理
217+
</Link>
218+
</Button>
219+
) : (
220+
<Button asChild variant="outline" className="flex items-center gap-2">
221+
<Link href="/">
222+
<Home className="size-4" />
223+
回到首頁
224+
</Link>
225+
</Button>
226+
)}
227+
</div>
228+
</CardContent>
229+
230+
<CardFooter
231+
className={`justify-center text-center text-xs text-muted-foreground`}
232+
>
233+
<section className="flex flex-col items-center gap-1">
234+
<p>如果問題持續發生,請聯絡開發者進行處理。</p>
235+
<p className="text-red-600">
236+
錯誤時間:{new Date().toLocaleString('zh-TW', { timeZone: 'Asia/Taipei' })}
237+
</p>
238+
{'digest' in error && error.digest && (
239+
<p className="text-red-600">錯誤 ID:{error.digest}</p>
240+
)}
241+
</section>
242+
</CardFooter>
243+
</Card>
244+
</div>
245+
</body>
246+
</html>
247+
);
248+
}

0 commit comments

Comments
 (0)