Commit 9151bfc2 authored by ThinhNC's avatar ThinhNC

feat: implement custom 404 not-found page with localized client component

parent 82f476d0
import { notFound } from "next/navigation";
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() {
notFound();
return <NotFoundClient />;
}
"use client";
import type { Metadata } from "next";
import { NotFoundClient } from "@/components/common/not-found-client";
import React 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 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() {
const pathname = usePathname();
const { t } = useLanguage();
// 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>
);
return <NotFoundClient />;
}
import { notFound } from "next/navigation";
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() {
notFound();
return <NotFoundClient />;
}
"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>
);
}
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