feat(web): add layout, nav, and error boundary (frontend-design)

This commit is contained in:
yiekheng 2026-05-02 21:01:15 +08:00
parent c0749d1af0
commit 7a5c00d08a
3 changed files with 147 additions and 2 deletions

65
web/app/error.tsx Normal file
View File

@ -0,0 +1,65 @@
"use client";
import { useEffect } from "react";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error("Top-level error boundary caught:", error);
}, [error]);
return (
<div className="mx-auto flex min-h-[60vh] max-w-2xl items-center justify-center px-4 py-12">
<div className="w-full border-2 border-zinc-900 bg-white">
<div className="flex items-center gap-3 border-b-2 border-zinc-900 bg-yellow-300 px-4 py-2">
<span
aria-hidden="true"
className="inline-flex h-5 w-5 items-center justify-center bg-zinc-900 text-xs font-bold text-yellow-300"
>
!
</span>
<span className="font-mono text-[11px] font-bold uppercase tracking-[0.25em] text-zinc-900">
api unreachable
</span>
</div>
<div className="px-6 py-6 sm:px-8 sm:py-8">
<h1 className="font-mono text-2xl font-bold uppercase tracking-tight text-zinc-900 sm:text-3xl">
Couldn&apos;t reach the API
</h1>
<p className="mt-3 text-sm text-zinc-700 sm:text-base">
The dashboard fetches data from{" "}
<code className="rounded-sm bg-zinc-900 px-1.5 py-0.5 font-mono text-xs text-yellow-300">
api-server:3000
</code>{" "}
on the internal docker network. The container may be down or
still starting. Wait a few seconds and retry.
</p>
<button
type="button"
onClick={reset}
className="mt-6 inline-flex items-center border-2 border-zinc-900 bg-yellow-300 px-4 py-2 font-mono text-[11px] font-bold uppercase tracking-[0.2em] text-zinc-900 hover:bg-zinc-900 hover:text-yellow-300"
>
Retry
</button>
<div className="mt-8 border-t-2 border-zinc-200 pt-4">
<div className="font-mono text-[10px] font-bold uppercase tracking-[0.25em] text-zinc-500">
error
</div>
<pre className="mt-2 overflow-x-auto rounded-sm bg-zinc-100 p-3 font-mono text-xs text-zinc-800">
{error.message}
{error.digest ? `\n\ndigest: ${error.digest}` : ""}
</pre>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,7 +1,21 @@
import "./globals.css"; import "./globals.css";
import type { Metadata, Viewport } from "next";
import Nav from "@/components/nav";
import AutoRefresh from "@/components/auto-refresh";
export const metadata = { export const metadata: Metadata = {
title: "CM Bot V2", title: "CM Bot V2",
description: "CM Bot account and user dashboard",
};
export const viewport: Viewport = {
themeColor: "#facc15",
};
const workbenchGrid = {
backgroundImage:
"radial-gradient(circle at 1px 1px, rgba(24,24,27,0.07) 1px, transparent 0)",
backgroundSize: "24px 24px",
}; };
export default function RootLayout({ export default function RootLayout({
@ -11,7 +25,14 @@ export default function RootLayout({
}) { }) {
return ( return (
<html lang="en"> <html lang="en">
<body>{children}</body> <body
className="min-h-screen bg-zinc-50 text-zinc-900 antialiased"
style={workbenchGrid}
>
<Nav />
<main>{children}</main>
<AutoRefresh />
</body>
</html> </html>
); );
} }

59
web/components/nav.tsx Normal file
View File

@ -0,0 +1,59 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
export default function Nav() {
const pathname = usePathname() ?? "/";
const isUsers = pathname.startsWith("/users");
const isAccounts = !isUsers;
return (
<header className="sticky top-0 z-10 border-b-2 border-zinc-900 bg-white/95 backdrop-blur">
<div className="mx-auto flex max-w-6xl items-center justify-between gap-4 px-4 py-3">
<div className="hidden flex-col leading-none sm:flex">
<span className="font-mono text-sm font-bold uppercase tracking-[0.2em] text-zinc-900">
CM Bot V2
</span>
<span className="mt-0.5 font-mono text-[10px] uppercase tracking-[0.3em] text-zinc-500">
// dashboard
</span>
</div>
<nav className="flex items-center gap-2" aria-label="Primary">
<NavLink href="/" active={isAccounts}>
Accounts
</NavLink>
<NavLink href="/users" active={isUsers}>
Users
</NavLink>
</nav>
</div>
</header>
);
}
function NavLink({
href,
active,
children,
}: {
href: string;
active: boolean;
children: React.ReactNode;
}) {
const base =
"inline-flex items-center border-2 px-3 py-1.5 font-mono text-[11px] font-bold uppercase tracking-[0.2em] transition-colors";
const activeCls = "border-zinc-900 bg-yellow-300 text-zinc-900";
const inactiveCls =
"border-transparent text-zinc-700 hover:border-zinc-900 hover:bg-yellow-50 hover:text-zinc-900";
return (
<Link
href={href}
aria-current={active ? "page" : undefined}
className={`${base} ${active ? activeCls : inactiveCls}`}
>
{children}
</Link>
);
}