Skip to content

Commit 6944f64

Browse files
authored
Merge pull request #25 from database-playground/improve-codebase
refactor: better error page
2 parents 1b56145 + de26405 commit 6944f64

File tree

2 files changed

+81
-89
lines changed

2 files changed

+81
-89
lines changed

app/global-error.tsx

Lines changed: 58 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -4,99 +4,88 @@ import { Logo } from "@/components/logo";
44
import { Badge } from "@/components/ui/badge";
55
import { Button } from "@/components/ui/button";
66
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
7-
import { ERROR_NOT_FOUND, ERROR_NOT_IMPLEMENTED, ERROR_UNAUTHORIZED, ERROR_USER_VERIFIED } from "@/lib/apollo-errors";
7+
import {
8+
ERROR_FORBIDDEN,
9+
ERROR_INVALID_INPUT,
10+
ERROR_NOT_FOUND,
11+
ERROR_NOT_IMPLEMENTED,
12+
ERROR_UNAUTHORIZED,
13+
} from "@/lib/apollo-errors";
814
import { CombinedGraphQLErrors, CombinedProtocolErrors } from "@apollo/client";
9-
import { AlertCircle, Code, Home, Lock, RefreshCw, Search, Shield, WifiOff } from "lucide-react";
15+
import { AlertCircle, Bug, Code, Home, Lock, type LucideIcon, Search, Shield } from "lucide-react";
1016
import Link from "next/link";
11-
import { useEffect } from "react";
1217

1318
interface GlobalErrorProps {
1419
error: Error & { digest?: string };
1520
reset: () => void;
1621
}
1722

18-
function getErrorInfo(error: Error) {
19-
if (CombinedProtocolErrors.is(error)) {
20-
// Network errors
21-
if (error && "statusCode" in error) {
22-
switch (error.statusCode) {
23-
case 401:
23+
type ErrorCommonInfo = {
24+
title: string;
25+
description: string;
26+
icon: LucideIcon;
27+
};
28+
29+
type ErrorActionInfo = {
30+
actionName: string;
31+
actionHref: string;
32+
};
33+
34+
type ErrorInfo = ErrorCommonInfo & (ErrorActionInfo | { actionName?: undefined; actionHref?: undefined });
35+
36+
function getErrorInfo(error: Error): ErrorInfo {
37+
// GraphQL errors with codes
38+
if (CombinedGraphQLErrors.is(error)) {
39+
const graphQLErrors = error.errors;
40+
41+
if (graphQLErrors && graphQLErrors.length > 0) {
42+
const firstError = graphQLErrors[0];
43+
const errorCode = firstError.extensions?.code as string;
44+
45+
switch (errorCode) {
46+
case ERROR_NOT_FOUND:
47+
return {
48+
title: "找不到資料",
49+
description: "請求的資料不存在或已被刪除。",
50+
icon: Search,
51+
};
52+
case ERROR_UNAUTHORIZED:
2453
return {
2554
title: "未經授權",
26-
description: "您的登入狀態已過期,請重新登入。",
55+
description: "請登入後再試,或您的權限不足。",
2756
icon: Lock,
57+
actionName: "重新登入",
2858
actionHref: "/login",
2959
};
30-
case 403:
60+
case ERROR_FORBIDDEN:
61+
const missingScopes = firstError.message.replace(/^no sufficient scope: /, "");
3162
return {
3263
title: "權限不足",
33-
description: "您沒有權限執行此操作。",
64+
description: "您的帳號缺少權限,請聯絡管理員開通權限後重新登入:" + missingScopes,
3465
icon: Shield,
66+
actionName: "重新登入",
67+
actionHref: "/login",
3568
};
36-
case 404:
69+
case ERROR_NOT_IMPLEMENTED:
3770
return {
38-
title: "找不到資源",
39-
description: "請求的資源不存在或已被移除。",
40-
icon: Search,
71+
title: "功能未實作",
72+
description: "此功能目前尚未實作,請稍後再試。",
73+
icon: Code,
4174
};
42-
case 500:
75+
case ERROR_INVALID_INPUT:
4376
return {
44-
title: "伺服器錯誤",
45-
description: "伺服器發生內部錯誤,請稍後再試。",
46-
icon: AlertCircle,
77+
title: "輸入無效",
78+
description: "請聯絡開發者檢查 API 的輸入資料。",
79+
icon: Bug,
4780
};
4881
}
4982

5083
return {
51-
title: "網路連線錯誤",
52-
description: "無法連接到伺服器,請檢查網路連線。",
53-
icon: WifiOff,
84+
title: "GraphQL 查詢錯誤",
85+
description: firstError.message || "GraphQL 查詢發生錯誤。",
86+
icon: AlertCircle,
5487
};
5588
}
56-
57-
// GraphQL errors with codes
58-
if (CombinedGraphQLErrors.is(error)) {
59-
const graphQLErrors = error.errors;
60-
61-
if (graphQLErrors && graphQLErrors.length > 0) {
62-
const firstError = graphQLErrors[0];
63-
const errorCode = firstError.extensions?.code as string;
64-
65-
switch (errorCode) {
66-
case ERROR_NOT_FOUND:
67-
return {
68-
title: "找不到資料",
69-
description: "請求的資料不存在或已被刪除。",
70-
icon: Search,
71-
};
72-
case ERROR_UNAUTHORIZED:
73-
return {
74-
title: "未經授權",
75-
description: "請登入後再試,或您的權限不足。",
76-
icon: Lock,
77-
actionHref: "/login",
78-
};
79-
case ERROR_USER_VERIFIED:
80-
return {
81-
title: "帳號已驗證",
82-
description: "此帳號已經完成驗證程序。",
83-
icon: Shield,
84-
};
85-
case ERROR_NOT_IMPLEMENTED:
86-
return {
87-
title: "功能未實作",
88-
description: "此功能目前尚未實作,請稍後再試。",
89-
icon: Code,
90-
};
91-
}
92-
93-
return {
94-
title: "GraphQL 查詢錯誤",
95-
description: firstError.message || "GraphQL 查詢發生錯誤。",
96-
icon: AlertCircle,
97-
};
98-
}
99-
}
10089
}
10190

10291
// Regular JavaScript errors
@@ -107,14 +96,9 @@ function getErrorInfo(error: Error) {
10796
};
10897
}
10998

110-
export default function GlobalError({ error, reset }: GlobalErrorProps) {
99+
export default function GlobalError({ error }: GlobalErrorProps) {
111100
const errorInfo = getErrorInfo(error);
112101

113-
useEffect(() => {
114-
// Log error to monitoring service
115-
console.error("Global error:", error);
116-
}, [error]);
117-
118102
return (
119103
<html>
120104
<body>
@@ -223,23 +207,14 @@ export default function GlobalError({ error, reset }: GlobalErrorProps) {
223207
sm:flex-row
224208
`}
225209
>
226-
<Button
227-
onClick={reset}
228-
variant="default"
229-
className={`flex items-center gap-2`}
230-
>
231-
<RefreshCw className="size-4" />
232-
重試
233-
</Button>
234-
235210
{errorInfo.actionHref
236211
? (
237212
<Button
238213
asChild
239214
variant="outline"
240215
className={`flex items-center gap-2`}
241216
>
242-
<Link href={errorInfo.actionHref}>前往處理</Link>
217+
<Link href={errorInfo.actionHref}>{errorInfo.actionName}</Link>
243218
</Button>
244219
)
245220
: (
@@ -270,11 +245,6 @@ export default function GlobalError({ error, reset }: GlobalErrorProps) {
270245
timeZone: "Asia/Taipei",
271246
})}
272247
</p>
273-
{error.digest && (
274-
<p className="text-red-600">
275-
錯誤 ID:{error.digest}
276-
</p>
277-
)}
278248
</section>
279249
</CardFooter>
280250
</Card>

lib/apollo-errors.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,26 @@
1+
/**
2+
* GraphQL API error codes.
3+
*
4+
* @see https://github.com/database-playground/backend-v2/blob/main/graph/defs/README.md
5+
*/
6+
7+
/**
8+
* NOT_FOUND: 找不到指定的實體。
9+
*/
110
export const ERROR_NOT_FOUND = "NOT_FOUND";
11+
/**
12+
* UNAUTHORIZED: 這個 API 需要認證或授權後才能運作。
13+
*/
214
export const ERROR_UNAUTHORIZED = "UNAUTHORIZED";
3-
export const ERROR_USER_VERIFIED = "USER_VERIFIED";
15+
/**
16+
* NOT_IMPLEMENTED: 這個 API 尚未實作,請先不要呼叫。
17+
*/
418
export const ERROR_NOT_IMPLEMENTED = "NOT_IMPLEMENTED";
19+
/**
20+
* FORBIDDEN: 使用者的權限 (scope) 不足以執行這個操作。
21+
*/
22+
export const ERROR_FORBIDDEN = "FORBIDDEN";
23+
/**
24+
* INVALID_INPUT: 輸入有誤。
25+
*/
26+
export const ERROR_INVALID_INPUT = "INVALID_INPUT";

0 commit comments

Comments
 (0)