Commit 59f3a92c authored by ThinhNC's avatar ThinhNC

Merge branch 'feat/header-ux-polish-and-page-titles' into 'develop'

Feat/header ux polish and page titles

See merge request !6
parents 59fee8b7 9151bfc2
import type { Metadata } from "next";
import { NotFoundClient } from "@/components/common/not-found-client";
export const metadata: Metadata = {
title: "Không Tìm Thấy Trang (404)",
description: "Trang bạn yêu cầu không tồn tại trong hệ thống Data Crawler",
};
export default function Page404() {
return <NotFoundClient />;
}
......@@ -19,20 +19,20 @@ export default function ErrorBoundary({
}, [error]);
return (
<div className="flex min-h-[60vh] flex-col items-center justify-center text-center px-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-400 mb-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-500 dark:text-red-400 mb-4 shadow-sm">
<AlertTriangle className="h-8 w-8" />
</div>
<h2 className="text-xl font-bold text-slate-100">Đã xảy ra sự cố không mong muốn</h2>
<p className="mt-2 text-sm text-slate-400 max-w-md">
<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-muted-foreground max-w-md leading-relaxed">
{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>
<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" />
Thử Lại
</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ủ
</Button>
</div>
......
......@@ -3,12 +3,12 @@ import { Loader2 } from "lucide-react";
export default function Loading() {
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="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-400 animate-pulse" />
<Loader2 className="absolute inset-0 m-auto h-5 w-5 text-emerald-500 animate-pulse" />
</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>
);
}
import React from "react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Compass, Home } from "lucide-react";
import type { Metadata } from "next";
import { NotFoundClient } from "@/components/common/not-found-client";
export const metadata: Metadata = {
title: "Không Tìm Thấy Trang (404)",
description: "Trang bạn yêu cầu không tồn tại trong hệ thống Data Crawler",
};
export default function NotFound() {
return (
<div className="flex min-h-[60vh] flex-col items-center justify-center text-center px-4">
<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" />
</div>
<h1 className="text-4xl font-extrabold tracking-tight text-white">404</h1>
<h2 className="mt-2 text-lg font-semibold text-slate-200">Không tìm thấy trang yêu cầu</h2>
<p className="mt-1 text-sm text-slate-400 max-w-sm">
Đườ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.
</p>
<div className="mt-6">
<Button asChild className="bg-emerald-600 hover:bg-emerald-500">
<Link href="/">
<Home className="mr-2 h-4 w-4" />
Về Dashboard
</Link>
</Button>
</div>
</div>
);
return <NotFoundClient />;
}
import type { Metadata } from "next";
import { NotFoundClient } from "@/components/common/not-found-client";
export const metadata: Metadata = {
title: "Không Tìm Thấy Trang (404)",
description: "Trang bạn yêu cầu không tồn tại trong hệ thống Data Crawler",
};
export default function NotFoundPage() {
return <NotFoundClient />;
}
......@@ -4,12 +4,14 @@ import React from "react";
import { usePathname } from "next/navigation";
import { Navbar } from "@/components/common/navbar";
const AUTH_PATHS = [
export const STANDALONE_PATHS = [
"/login",
"/register",
"/forgot-password",
"/reset-password",
"/verify-email",
"/not-found",
"/404",
];
interface AppShellProps {
......@@ -18,10 +20,10 @@ interface AppShellProps {
export function AppShell({ children }: AppShellProps) {
const pathname = usePathname();
const isAuthPage = AUTH_PATHS.some((path) => pathname.startsWith(path));
const isStandalonePage = STANDALONE_PATHS.some((path) => pathname.startsWith(path));
// If on an auth page, hide the main Navbar and default footer completely
if (isAuthPage) {
// If on an auth or standalone page, hide the main Navbar and default footer
if (isStandalonePage) {
return <main className="flex-1 flex flex-col min-h-screen">{children}</main>;
}
......
"use client";
import React, { useEffect } from "react";
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";
export function NotFoundClient() {
const pathname = usePathname();
const { t } = useLanguage();
// Dynamically update browser tab title to reflect language switch
useEffect(() => {
const title = `${t.auth.notFound.title} (404) | Data Crawler`;
document.title = title;
}, [t]);
// Determine if this 404 page is accessed standalone (outside the dashboard layout / no Navbar)
const isStandalone =
!pathname ||
STANDALONE_PATHS.some((path) => pathname.startsWith(path)) ||
pathname === "/" ||
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>
{/* Single Action Button: Return Home */}
<div>
<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" />
{t.auth.notFound.backHome}
</Button>
</Link>
</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>
);
}
......@@ -149,7 +149,7 @@ export const translations = {
confirmPasswordPlaceholder: "••••••••",
submit: "Cập Nhật Mật Khẩu",
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!",
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",
......@@ -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.",
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: {
......@@ -371,6 +376,11 @@ export const translations = {
subtitle: "You do not have permission to access this administration page. Please contact an Administrator (ADMIN).",
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",
},
},
},
};
......
......@@ -18,9 +18,11 @@ const ADMIN_ROUTES = [
"/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 = [
"/forbidden",
"/not-found",
"/404",
];
/**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment