Skip to content

Commit ce2483c

Browse files
committed
better not found page
1 parent b87ffc1 commit ce2483c

File tree

2 files changed

+408
-64
lines changed

2 files changed

+408
-64
lines changed

apps/dashboard/app/not-found.tsx

Lines changed: 212 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,231 @@
1-
import { HouseIcon } from "@phosphor-icons/react/ssr";
1+
"use client";
2+
3+
import {
4+
billingNavigation,
5+
organizationNavigation,
6+
personalNavigation,
7+
resourcesNavigation,
8+
} from "@/components/layout/navigation/navigation-config";
9+
import type { NavigationItem, NavigationSection } from "@/components/layout/navigation/types";
10+
import { ArrowLeftIcon, CommandIcon, HouseIcon, MagnifyingGlassIcon } from "@phosphor-icons/react";
11+
import { Command as CommandPrimitive } from "cmdk";
212
import Link from "next/link";
3-
import { Logo } from "@/components/layout/logo";
13+
import { useRouter } from "next/navigation";
14+
import { useMemo, useState } from "react";
415
import { Button } from "@/components/ui/button";
5-
import "./not-found.css";
16+
import { Card, CardContent } from "@/components/ui/card";
17+
import {
18+
Dialog,
19+
DialogContent,
20+
DialogDescription,
21+
DialogHeader,
22+
DialogTitle,
23+
} from "@/components/ui/dialog";
24+
import { cn } from "@/lib/utils";
25+
26+
const ALL_NAVIGATION: NavigationSection[] = [
27+
...organizationNavigation,
28+
...billingNavigation,
29+
...personalNavigation,
30+
...resourcesNavigation,
31+
];
32+
33+
interface SearchItem {
34+
name: string;
35+
path: string;
36+
icon: typeof MagnifyingGlassIcon;
37+
}
38+
39+
function toSearchItem(item: NavigationItem): SearchItem | null {
40+
if (item.disabled || item.hideFromDemo || !item.href) {
41+
return null;
42+
}
43+
return {
44+
name: item.name,
45+
path: item.href,
46+
icon: item.icon || MagnifyingGlassIcon,
47+
};
48+
}
49+
50+
function flattenNavigation(sections: NavigationSection[]): SearchItem[] {
51+
const items: SearchItem[] = [];
52+
for (const section of sections) {
53+
for (const item of section.items) {
54+
const searchItem = toSearchItem(item);
55+
if (searchItem) {
56+
items.push(searchItem);
57+
}
58+
}
59+
}
60+
return items;
61+
}
662

763
export default function NotFound() {
8-
return (
9-
<div className="flex h-screen flex-col items-center justify-center bg-background p-4">
10-
<div className="absolute top-8 right-0 left-0 flex justify-center">
11-
<div className="flex items-center gap-2">
12-
<Logo />
13-
</div>
14-
</div>
64+
const router = useRouter();
65+
const [open, setOpen] = useState(false);
66+
const [search, setSearch] = useState("");
1567

16-
<div className="flex w-full max-w-md flex-col items-center">
17-
<div className="mb-4 flex items-baseline font-mono">
18-
<span className="font-bold text-8xl text-primary md:text-9xl">4</span>
19-
<div className="relative mx-2">
20-
<span className="cycling-digit animate-pulse font-bold text-8xl text-primary md:text-9xl" />
21-
<div className="-z-10 absolute inset-0 rounded-full bg-primary/10 blur-xl" />
22-
</div>
23-
<span className="font-bold text-8xl text-primary md:text-9xl">4</span>
24-
</div>
68+
const searchItems = useMemo(() => {
69+
const items = flattenNavigation(ALL_NAVIGATION);
70+
if (!search.trim()) {
71+
return items;
72+
}
73+
const query = search.toLowerCase();
74+
return items.filter(
75+
(item) =>
76+
item.name.toLowerCase().includes(query) ||
77+
item.path.toLowerCase().includes(query)
78+
);
79+
}, [search]);
80+
81+
const handleSelect = (item: SearchItem) => {
82+
setOpen(false);
83+
setSearch("");
84+
router.push(item.path);
85+
};
2586

26-
<div className="mb-4 h-px w-16 bg-border" />
87+
const canGoBack = typeof window !== "undefined" && window.history.length > 1;
2788

28-
<div className="mb-6 text-4xl">
29-
<span
30-
aria-label="sad face"
31-
className="-rotate-90 inline-block transform text-5xl"
89+
return (
90+
<div className="flex min-h-screen flex-col items-center justify-center bg-background p-4 sm:p-6 lg:p-8">
91+
<Card className="flex w-full max-w-md flex-1 flex-col items-center justify-center rounded border-none bg-transparent shadow-none">
92+
<CardContent className="flex flex-col items-center justify-center text-center px-6 sm:px-8 lg:px-12 py-12 sm:py-14">
93+
<div
94+
aria-hidden="true"
95+
className="flex size-12 items-center justify-center rounded-2xl bg-accent"
3296
role="img"
3397
>
34-
:(
35-
</span>
36-
</div>
37-
38-
<h1 className="mb-2 text-center font-bold text-2xl md:text-3xl">
39-
Page Not Found
40-
</h1>
98+
<MagnifyingGlassIcon
99+
aria-hidden="true"
100+
className="size-6 text-muted-foreground"
101+
size={24}
102+
weight="fill"
103+
/>
104+
</div>
41105

42-
<p className="mb-8 text-center text-muted-foreground">
43-
We&apos;ve lost this page in the data stream.
44-
</p>
106+
<div className="mt-6 space-y-4 max-w-sm w-full">
107+
<h1 className="font-semibold text-foreground text-lg">
108+
Page Not Found
109+
</h1>
110+
<p className="text-muted-foreground text-sm leading-relaxed text-balance">
111+
We&apos;ve lost this page in the data stream.
112+
</p>
113+
</div>
45114

46-
<div className="flex w-full max-w-xs flex-col gap-4 sm:flex-row">
47115
<Button
48-
asChild
49-
className="flex-1 bg-primary hover:bg-primary/90"
50-
variant="default"
116+
className="mt-6 w-full max-w-xs"
117+
onClick={() => setOpen(true)}
118+
variant="outline"
51119
>
52-
<Link href="/websites">
53-
<HouseIcon className="size-4" size={16} weight="duotone" />
54-
Home
55-
</Link>
120+
<MagnifyingGlassIcon className="mr-2 size-4" weight="duotone" />
121+
Search pages, settings...
122+
<kbd className="ml-auto hidden items-center gap-1 rounded border bg-background px-1.5 py-0.5 font-mono text-muted-foreground text-xs sm:flex">
123+
<CommandIcon className="size-3" weight="bold" />
124+
<span>K</span>
125+
</kbd>
56126
</Button>
57-
</div>
58-
</div>
59127

60-
<div className="absolute bottom-8 rounded-md border border-accent bg-accent/50 px-4 py-2 font-mono text-muted-foreground text-xs">
61-
<code>ERR_PAGE_NOT_FOUND</code>
62-
</div>
128+
<Dialog onOpenChange={setOpen} open={open}>
129+
<DialogHeader className="sr-only">
130+
<DialogTitle>Search</DialogTitle>
131+
<DialogDescription>Search for pages and settings</DialogDescription>
132+
</DialogHeader>
133+
<DialogContent
134+
className="gap-0 overflow-hidden p-0 sm:max-w-xl"
135+
showCloseButton={false}
136+
>
137+
<CommandPrimitive
138+
className="flex h-full w-full flex-col"
139+
loop
140+
onKeyDown={(e) => {
141+
if (e.key === "Escape") {
142+
setOpen(false);
143+
}
144+
}}
145+
>
146+
<div className="dotted-bg flex items-center gap-3 border-b bg-accent px-4 py-3">
147+
<div className="flex size-8 shrink-0 items-center justify-center rounded bg-background">
148+
<MagnifyingGlassIcon
149+
className="size-4 text-muted-foreground"
150+
weight="duotone"
151+
/>
152+
</div>
153+
<CommandPrimitive.Input
154+
className="h-8 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
155+
onValueChange={setSearch}
156+
placeholder="Search pages, settings..."
157+
value={search}
158+
/>
159+
<kbd className="hidden items-center gap-1 rounded border bg-background px-1.5 py-0.5 font-mono text-muted-foreground text-xs sm:flex">
160+
<CommandIcon className="size-3" weight="bold" />
161+
<span>K</span>
162+
</kbd>
163+
</div>
63164

64-
<div className="pointer-events-none absolute inset-0 overflow-hidden opacity-5">
65-
<div className="-right-24 -top-24 absolute h-96 w-96 rounded-full border-8 border-primary border-dashed" />
66-
<div className="-left-24 -bottom-24 absolute h-96 w-96 rounded-full border-8 border-primary border-dashed" />
67-
</div>
165+
<CommandPrimitive.List className="max-h-80 overflow-y-auto scroll-py-2 p-2">
166+
<CommandPrimitive.Empty className="flex flex-col items-center justify-center gap-2 py-12 text-center">
167+
<MagnifyingGlassIcon
168+
className="size-8 text-muted-foreground/50"
169+
weight="duotone"
170+
/>
171+
<div>
172+
<p className="font-medium text-muted-foreground text-sm">No results found</p>
173+
<p className="text-muted-foreground/70 text-xs">
174+
Try searching for something else
175+
</p>
176+
</div>
177+
</CommandPrimitive.Empty>
178+
{searchItems.map((item) => {
179+
const ItemIcon = item.icon;
180+
return (
181+
<CommandPrimitive.Item
182+
className={cn(
183+
"group relative flex cursor-pointer select-none items-center gap-3 rounded px-2 py-2 outline-none transition-colors",
184+
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground"
185+
)}
186+
key={item.path}
187+
onSelect={() => handleSelect(item)}
188+
value={`${item.name} ${item.path}`}
189+
>
190+
<div className="flex size-7 shrink-0 items-center justify-center rounded bg-accent transition-colors group-data-[selected=true]:bg-background">
191+
<ItemIcon className="size-4 text-muted-foreground" weight="duotone" />
192+
</div>
193+
<div className="min-w-0 flex-1">
194+
<p className="truncate font-medium text-sm leading-tight">{item.name}</p>
195+
<p className="truncate text-muted-foreground text-xs">{item.path}</p>
196+
</div>
197+
</CommandPrimitive.Item>
198+
);
199+
})}
200+
</CommandPrimitive.List>
201+
</CommandPrimitive>
202+
</DialogContent>
203+
</Dialog>
204+
205+
<div className="mt-6 flex w-full max-w-xs flex-col items-stretch justify-center gap-3 sm:flex-row sm:items-center">
206+
{canGoBack && (
207+
<Button
208+
className="flex-1"
209+
onClick={() => router.back()}
210+
variant="outline"
211+
>
212+
<ArrowLeftIcon className="mr-2 size-4" weight="duotone" />
213+
Go Back
214+
</Button>
215+
)}
216+
<Button
217+
asChild
218+
className={canGoBack ? "flex-1 bg-primary hover:bg-primary/90" : "w-full bg-primary hover:bg-primary/90"}
219+
variant="default"
220+
>
221+
<Link href="/websites">
222+
<HouseIcon className="mr-2 size-4" weight="duotone" />
223+
Back to Websites
224+
</Link>
225+
</Button>
226+
</div>
227+
</CardContent>
228+
</Card>
68229
</div>
69230
);
70231
}

0 commit comments

Comments
 (0)