feat(web): add layout, nav, and error boundary (frontend-design)
This commit is contained in:
parent
c0749d1af0
commit
7a5c00d08a
65
web/app/error.tsx
Normal file
65
web/app/error.tsx
Normal 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'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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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
59
web/components/nav.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user