Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
U
upgrade-data-crawler-fe
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ThinhNC
upgrade-data-crawler-fe
Commits
82f476d0
Commit
82f476d0
authored
Sep 06, 2026
by
ThinhNC
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: implement error, loading, and 404 page handlers with app shell and middleware support
parent
c54b1110
Changes
8
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
140 additions
and
29 deletions
+140
-29
page.tsx
src/app/404/page.tsx
+5
-0
error.tsx
src/app/error.tsx
+6
-6
loading.tsx
src/app/loading.tsx
+3
-3
not-found.tsx
src/app/not-found.tsx
+101
-14
page.tsx
src/app/not-found/page.tsx
+5
-0
app-shell.tsx
src/components/common/app-shell.tsx
+6
-4
translations.ts
src/lib/i18n/translations.ts
+11
-1
middleware.ts
src/middleware.ts
+3
-1
No files found.
src/app/404/page.tsx
0 → 100644
View file @
82f476d0
import
{
notFound
}
from
"next/navigation"
;
export
default
function
Page404
()
{
notFound
();
}
src/app/error.tsx
View file @
82f476d0
...
@@ -19,20 +19,20 @@ export default function ErrorBoundary({
...
@@ -19,20 +19,20 @@ export default function ErrorBoundary({
},
[
error
]);
},
[
error
]);
return
(
return
(
<
div
className=
"flex min-h-[
60vh] flex-col items-center justify-center text-center px-4
"
>
<
div
className=
"flex min-h-[
calc(100vh-14rem)] flex-col items-center justify-center text-center px-4 py-8
"
>
<
div
className=
"flex h-16 w-16 items-center justify-center rounded-2xl bg-red-500/10 border border-red-500/20 text-red-
400 mb-4
"
>
<
div
className=
"flex h-16 w-16 items-center justify-center rounded-2xl bg-red-500/10 border border-red-500/20 text-red-
500 dark:text-red-400 mb-4 shadow-sm
"
>
<
AlertTriangle
className=
"h-8 w-8"
/>
<
AlertTriangle
className=
"h-8 w-8"
/>
</
div
>
</
div
>
<
h2
className=
"text-
xl font-bold text-slate-100
"
>
Đã xảy ra sự cố không mong muốn
</
h2
>
<
h2
className=
"text-
2xl font-bold text-foreground tracking-tight
"
>
Đã xảy ra sự cố không mong muốn
</
h2
>
<
p
className=
"mt-2 text-sm text-
slate-400 max-w-m
d"
>
<
p
className=
"mt-2 text-sm text-
muted-foreground max-w-md leading-relaxe
d"
>
{
error
?.
message
||
"Hệ thống gặp lỗi khi tải trang này. Bạn có thể thử khôi phục lại."
}
{
error
?.
message
||
"Hệ thống gặp lỗi khi tải trang này. Bạn có thể thử khôi phục lại."
}
</
p
>
</
p
>
<
div
className=
"mt-6 flex items-center gap-3"
>
<
div
className=
"mt-6 flex items-center gap-3"
>
<
Button
onClick=
{
()
=>
reset
()
}
className=
"
bg-emerald-600 hover:bg-emerald-500
"
>
<
Button
onClick=
{
()
=>
reset
()
}
className=
"
rounded-2xl bg-emerald-600 hover:bg-emerald-700 text-white cursor-pointer
"
>
<
RefreshCw
className=
"mr-2 h-4 w-4"
/>
<
RefreshCw
className=
"mr-2 h-4 w-4"
/>
Thử Lại
Thử Lại
</
Button
>
</
Button
>
<
Button
variant=
"outline"
onClick=
{
()
=>
router
.
push
(
"/"
)
}
>
<
Button
variant=
"outline"
onClick=
{
()
=>
router
.
push
(
"/"
)
}
className=
"rounded-2xl border-border text-foreground hover:bg-muted cursor-pointer"
>
Về Trang Chủ
Về Trang Chủ
</
Button
>
</
Button
>
</
div
>
</
div
>
...
...
src/app/loading.tsx
View file @
82f476d0
...
@@ -3,12 +3,12 @@ import { Loader2 } from "lucide-react";
...
@@ -3,12 +3,12 @@ import { Loader2 } from "lucide-react";
export
default
function
Loading
()
{
export
default
function
Loading
()
{
return
(
return
(
<
div
className=
"flex min-h-[
60vh
] flex-col items-center justify-center gap-3"
>
<
div
className=
"flex min-h-[
calc(100vh-14rem)
] flex-col items-center justify-center gap-3"
>
<
div
className=
"relative"
>
<
div
className=
"relative"
>
<
div
className=
"h-12 w-12 rounded-full border-2 border-emerald-500/20 border-t-emerald-500 animate-spin"
/>
<
div
className=
"h-12 w-12 rounded-full border-2 border-emerald-500/20 border-t-emerald-500 animate-spin"
/>
<
Loader2
className=
"absolute inset-0 m-auto h-5 w-5 text-emerald-
4
00 animate-pulse"
/>
<
Loader2
className=
"absolute inset-0 m-auto h-5 w-5 text-emerald-
5
00 animate-pulse"
/>
</
div
>
</
div
>
<
p
className=
"text-sm font-medium text-
slate-400
"
>
Đang chuẩn bị dữ liệu...
</
p
>
<
p
className=
"text-sm font-medium text-
muted-foreground
"
>
Đang chuẩn bị dữ liệu...
</
p
>
</
div
>
</
div
>
);
);
}
}
src/app/not-found.tsx
View file @
82f476d0
"use client"
;
import
React
from
"react"
;
import
React
from
"react"
;
import
Link
from
"next/link"
;
import
Link
from
"next/link"
;
import
{
usePathname
}
from
"next/navigation"
;
import
{
Compass
,
Home
,
Bot
}
from
"lucide-react"
;
import
{
useLanguage
}
from
"@/providers/language-provider"
;
import
{
ThemeToggle
}
from
"@/components/common/theme-toggle"
;
import
{
LanguageToggle
}
from
"@/components/common/language-toggle"
;
import
{
STANDALONE_PATHS
}
from
"@/components/common/app-shell"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Compass
,
Home
}
from
"lucide-react"
;
export
default
function
NotFound
()
{
export
default
function
NotFound
()
{
return
(
const
pathname
=
usePathname
();
<
div
className=
"flex min-h-[60vh] flex-col items-center justify-center text-center px-4"
>
const
{
t
}
=
useLanguage
();
<
div
className=
"flex h-16 w-16 items-center justify-center rounded-2xl bg-slate-800/80 border border-slate-700 text-emerald-400 mb-4"
>
<
Compass
className=
"h-8 w-8 animate-pulse"
/>
// Determine if this 404 page is accessed standalone (outside the dashboard layout / no Navbar)
</
div
>
const
isStandalone
=
<
h1
className=
"text-4xl font-extrabold tracking-tight text-white"
>
404
</
h1
>
!
pathname
||
<
h2
className=
"mt-2 text-lg font-semibold text-slate-200"
>
Không tìm thấy trang yêu cầu
</
h2
>
STANDALONE_PATHS
.
some
((
path
)
=>
pathname
.
startsWith
(
path
))
||
<
p
className=
"mt-1 text-sm text-slate-400 max-w-sm"
>
pathname
===
"/"
||
Đường dẫn bạn truy cập không tồn tại hoặc đã được di chuyển sang địa chỉ khác.
pathname
===
"/not-found"
||
pathname
===
"/404"
;
const
content
=
(
<
div
className=
"flex flex-col items-center justify-center text-center max-w-lg mx-auto"
>
{
/* 404 Glowing Compass Icon */
}
<
div
className=
"relative mb-6"
>
<
div
className=
"absolute -inset-4 rounded-full bg-emerald-500/15 blur-xl"
/>
<
div
className=
"relative flex h-20 w-20 items-center justify-center rounded-3xl border border-emerald-500/30 bg-card shadow-lg shadow-emerald-950/10"
>
<
Compass
className=
"h-10 w-10 text-emerald-600 dark:text-emerald-400 animate-[spin_12s_linear_infinite]"
/>
</
div
>
</
div
>
{
/* 404 Badge */
}
<
div
className=
"inline-flex items-center rounded-full border border-emerald-500/20 bg-emerald-500/10 px-3 py-1 text-xs font-semibold text-emerald-600 dark:text-emerald-400 mb-4"
>
<
span
>
404
</
span
>
</
div
>
{
/* Title with Theme-Aware High Contrast */
}
<
h1
className=
"text-3xl font-extrabold tracking-tight text-foreground sm:text-4xl mb-3"
>
{
t
.
auth
.
notFound
.
title
}
</
h1
>
{
/* Subtitle / Description */
}
<
p
className=
"max-w-md text-sm text-muted-foreground mb-8 leading-relaxed"
>
{
t
.
auth
.
notFound
.
subtitle
}
</
p
>
</
p
>
<
div
className=
"mt-6"
>
<
Button
asChild
className=
"bg-emerald-600 hover:bg-emerald-500"
>
{
/* Single Action Button: Return Home */
}
<
div
>
<
Link
href=
"/"
>
<
Link
href=
"/"
>
<
Button
size=
"lg"
variant=
"default"
className=
"rounded-2xl bg-emerald-600 hover:bg-emerald-700 text-white font-medium cursor-pointer shadow-sm shadow-emerald-600/20 px-6"
>
<
Home
className=
"mr-2 h-4 w-4"
/>
<
Home
className=
"mr-2 h-4 w-4"
/>
Về Dashboard
{
t
.
auth
.
notFound
.
backHome
}
</
Link
>
</
Button
>
</
Button
>
</
Link
>
</
div
>
</
div
>
</
div
>
);
// If accessed outside dashboard or in standalone auth mode, wrap with dedicated header & footer controls
if
(
isStandalone
)
{
return
(
<
div
className=
"relative flex min-h-screen flex-col justify-between p-4 sm:p-6 overflow-hidden"
>
{
/* Decorative Biophilic Background Glows */
}
<
div
className=
"pointer-events-none absolute -top-24 left-1/2 -z-10 h-[500px] w-[500px] -translate-x-1/2 rounded-full bg-emerald-500/10 blur-3xl dark:bg-emerald-500/15"
/>
<
div
className=
"pointer-events-none absolute -bottom-24 right-1/4 -z-10 h-96 w-96 rounded-full bg-teal-500/10 blur-3xl dark:bg-teal-500/10"
/>
{
/* Minimal Header with Logo, Language & Theme Controls */
}
<
header
className=
"flex w-full items-center justify-between"
>
<
Link
href=
"/"
className=
"flex items-center gap-2.5 transition-opacity hover:opacity-90"
>
<
div
className=
"flex h-9 w-9 items-center justify-center rounded-2xl bg-gradient-to-br from-emerald-500 to-teal-700 shadow-md shadow-emerald-500/20 text-white"
>
<
Bot
className=
"h-5 w-5"
/>
</
div
>
<
span
className=
"font-bold text-base tracking-tight text-foreground"
>
DataCrawler
</
span
>
</
Link
>
{
/* Theme & Language Controls */
}
<
div
className=
"flex items-center gap-2"
>
<
LanguageToggle
/>
<
ThemeToggle
/>
</
div
>
</
header
>
{
/* Centered Main 404 Hero */
}
<
div
className=
"my-auto flex items-center justify-center py-8"
>
{
content
}
</
div
>
{
/* Clean Minimal Footer */
}
<
footer
className=
"text-center text-[11px] text-muted-foreground/80 py-2"
>
Data Crawler ©
{
new
Date
().
getFullYear
()
}
- Code by @hnihTyoB
</
footer
>
</
div
>
);
}
// If accessed inside the main dashboard (where Navbar and default footer are already rendered by AppShell)
return
(
<
div
className=
"flex min-h-[calc(100vh-14rem)] flex-col items-center justify-center text-center px-4 py-8"
>
{
content
}
</
div
>
</
div
>
);
);
}
}
src/app/not-found/page.tsx
0 → 100644
View file @
82f476d0
import
{
notFound
}
from
"next/navigation"
;
export
default
function
NotFoundPage
()
{
notFound
();
}
src/components/common/app-shell.tsx
View file @
82f476d0
...
@@ -4,12 +4,14 @@ import React from "react";
...
@@ -4,12 +4,14 @@ import React from "react";
import
{
usePathname
}
from
"next/navigation"
;
import
{
usePathname
}
from
"next/navigation"
;
import
{
Navbar
}
from
"@/components/common/navbar"
;
import
{
Navbar
}
from
"@/components/common/navbar"
;
const
AUTH
_PATHS
=
[
export
const
STANDALONE
_PATHS
=
[
"/login"
,
"/login"
,
"/register"
,
"/register"
,
"/forgot-password"
,
"/forgot-password"
,
"/reset-password"
,
"/reset-password"
,
"/verify-email"
,
"/verify-email"
,
"/not-found"
,
"/404"
,
];
];
interface
AppShellProps
{
interface
AppShellProps
{
...
@@ -18,10 +20,10 @@ interface AppShellProps {
...
@@ -18,10 +20,10 @@ interface AppShellProps {
export
function
AppShell
({
children
}:
AppShellProps
)
{
export
function
AppShell
({
children
}:
AppShellProps
)
{
const
pathname
=
usePathname
();
const
pathname
=
usePathname
();
const
is
AuthPage
=
AUTH
_PATHS
.
some
((
path
)
=>
pathname
.
startsWith
(
path
));
const
is
StandalonePage
=
STANDALONE
_PATHS
.
some
((
path
)
=>
pathname
.
startsWith
(
path
));
// If on an auth
page, hide the main Navbar and default footer completely
// If on an auth
or standalone page, hide the main Navbar and default footer
if
(
is
Auth
Page
)
{
if
(
is
Standalone
Page
)
{
return
<
main
className=
"flex-1 flex flex-col min-h-screen"
>
{
children
}
</
main
>;
return
<
main
className=
"flex-1 flex flex-col min-h-screen"
>
{
children
}
</
main
>;
}
}
...
...
src/lib/i18n/translations.ts
View file @
82f476d0
...
@@ -149,7 +149,7 @@ export const translations = {
...
@@ -149,7 +149,7 @@ export const translations = {
confirmPasswordPlaceholder
:
"••••••••"
,
confirmPasswordPlaceholder
:
"••••••••"
,
submit
:
"Cập Nhật Mật Khẩu"
,
submit
:
"Cập Nhật Mật Khẩu"
,
submitting
:
"Đang cập nhật..."
,
submitting
:
"Đang cập nhật..."
,
missingToken
:
"Thiếu mã xác thực
(Token)
. Vui lòng sử dụng đường link gửi qua email."
,
missingToken
:
"Thiếu mã xác thực. Vui lòng sử dụng đường link gửi qua email."
,
successTitle
:
"Cập Nhật Mật Khẩu Thành Công!"
,
successTitle
:
"Cập Nhật Mật Khẩu Thành Công!"
,
successDesc
:
"Mật khẩu của bạn đã được thay đổi. Hãy đăng nhập lại bằng mật khẩu mới."
,
successDesc
:
"Mật khẩu của bạn đã được thay đổi. Hãy đăng nhập lại bằng mật khẩu mới."
,
backToLogin
:
"Chuyển đến trang Đăng nhập"
,
backToLogin
:
"Chuyển đến trang Đăng nhập"
,
...
@@ -185,6 +185,11 @@ export const translations = {
...
@@ -185,6 +185,11 @@ export const translations = {
subtitle
:
"Bạn không có quyền truy cập trang quản trị này. Vui lòng liên hệ Quản trị viên (ADMIN) để được cấp quyền."
,
subtitle
:
"Bạn không có quyền truy cập trang quản trị này. Vui lòng liên hệ Quản trị viên (ADMIN) để được cấp quyền."
,
backHome
:
"Quay lại Bảng điều khiển"
,
backHome
:
"Quay lại Bảng điều khiển"
,
},
},
notFound
:
{
title
:
"Không tìm thấy trang yêu cầu"
,
subtitle
:
"Đường dẫn bạn truy cập không tồn tại, đã bị gỡ bỏ hoặc chuyển sang địa chỉ khác trong hệ thống."
,
backHome
:
"Về Trang Chủ"
,
},
},
},
},
},
en
:
{
en
:
{
...
@@ -371,6 +376,11 @@ export const translations = {
...
@@ -371,6 +376,11 @@ export const translations = {
subtitle
:
"You do not have permission to access this administration page. Please contact an Administrator (ADMIN)."
,
subtitle
:
"You do not have permission to access this administration page. Please contact an Administrator (ADMIN)."
,
backHome
:
"Back to Dashboard"
,
backHome
:
"Back to Dashboard"
,
},
},
notFound
:
{
title
:
"Page Not Found"
,
subtitle
:
"The page you are looking for does not exist, has been removed, or moved to another URL."
,
backHome
:
"Back to Home"
,
},
},
},
},
},
};
};
...
...
src/middleware.ts
View file @
82f476d0
...
@@ -18,9 +18,11 @@ const ADMIN_ROUTES = [
...
@@ -18,9 +18,11 @@ const ADMIN_ROUTES = [
"/roles"
,
"/roles"
,
];
];
// Open public pages that do NOT require login (e.g. 403 forbidden
page
)
// Open public pages that do NOT require login (e.g. 403 forbidden
, 404 not-found
)
const
OPEN_ROUTES
=
[
const
OPEN_ROUTES
=
[
"/forbidden"
,
"/forbidden"
,
"/not-found"
,
"/404"
,
];
];
/**
/**
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment