Skip to content

Commit 34336bb

Browse files
committed
fix: better error pages
1 parent ce2483c commit 34336bb

File tree

2 files changed

+295
-74
lines changed

2 files changed

+295
-74
lines changed

apps/dashboard/app/error.tsx

Lines changed: 233 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,63 @@
11
"use client";
22

3-
import { WarningCircleIcon } from "@phosphor-icons/react";
4-
import { useEffect } from "react";
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, WarningCircleIcon } from "@phosphor-icons/react";
11+
import { Command as CommandPrimitive } from "cmdk";
12+
import { useEffect, useMemo, useState } from "react";
13+
import { useRouter } from "next/navigation";
514
import { Button } from "@/components/ui/button";
15+
import { Card, CardContent } from "@/components/ui/card";
16+
import {
17+
Dialog,
18+
DialogContent,
19+
DialogDescription,
20+
DialogHeader,
21+
DialogTitle,
22+
} from "@/components/ui/dialog";
23+
import { cn } from "@/lib/utils";
24+
25+
const ALL_NAVIGATION: NavigationSection[] = [
26+
...organizationNavigation,
27+
...billingNavigation,
28+
...personalNavigation,
29+
...resourcesNavigation,
30+
];
31+
32+
interface SearchItem {
33+
name: string;
34+
path: string;
35+
icon: typeof MagnifyingGlassIcon;
36+
}
37+
38+
function toSearchItem(item: NavigationItem): SearchItem | null {
39+
if (item.disabled || item.hideFromDemo || !item.href) {
40+
return null;
41+
}
42+
return {
43+
name: item.name,
44+
path: item.href,
45+
icon: item.icon || MagnifyingGlassIcon,
46+
};
47+
}
48+
49+
function flattenNavigation(sections: NavigationSection[]): SearchItem[] {
50+
const items: SearchItem[] = [];
51+
for (const section of sections) {
52+
for (const item of section.items) {
53+
const searchItem = toSearchItem(item);
54+
if (searchItem) {
55+
items.push(searchItem);
56+
}
57+
}
58+
}
59+
return items;
60+
}
661

762
export default function GlobalError({
863
error,
@@ -11,43 +66,189 @@ export default function GlobalError({
1166
error: Error & { digest?: string };
1267
reset: () => void;
1368
}) {
69+
const router = useRouter();
70+
const [open, setOpen] = useState(false);
71+
const [search, setSearch] = useState("");
72+
1473
useEffect(() => {
1574
console.error("Global error occurred:", error);
1675
}, [error]);
1776

18-
const handleGoToHomepage = () => {
19-
window.location.href = "/";
77+
const searchItems = useMemo(() => {
78+
const items = flattenNavigation(ALL_NAVIGATION);
79+
if (!search.trim()) {
80+
return items;
81+
}
82+
const query = search.toLowerCase();
83+
return items.filter(
84+
(item) =>
85+
item.name.toLowerCase().includes(query) ||
86+
item.path.toLowerCase().includes(query)
87+
);
88+
}, [search]);
89+
90+
const handleSelect = (item: SearchItem) => {
91+
setOpen(false);
92+
setSearch("");
93+
router.push(item.path);
2094
};
2195

96+
const canGoBack = typeof window !== "undefined" && window.history.length > 1;
97+
2298
return (
23-
<div className="flex min-h-screen flex-col items-center justify-center bg-background p-4 text-foreground">
24-
<div className="max-w-md text-center">
25-
<WarningCircleIcon
26-
className="mx-auto mb-6 text-destructive"
27-
size={52}
28-
weight="duotone"
29-
/>
30-
<h1 className="mb-3 font-semibold text-3xl">Something went wrong</h1>
31-
<p className="mb-1 text-muted-foreground">
32-
We encountered an unexpected issue. Please try again.
33-
</p>
34-
{error?.message && (
35-
<p className="mx-auto my-3 w-fit rounded-md bg-destructive p-2 text-sm text-white">
36-
Error details: {error.message}
37-
</p>
38-
)}
39-
<Button className="mt-6" onClick={() => reset()} size="lg">
40-
Try again
41-
</Button>
42-
<Button
43-
className="mt-3 ml-3"
44-
onClick={handleGoToHomepage}
45-
size="lg"
46-
variant="outline"
47-
>
48-
Go to Homepage
49-
</Button>
50-
</div>
99+
<div className="flex min-h-screen flex-col items-center justify-center bg-background p-4 sm:p-6 lg:p-8">
100+
<Card className="flex w-full max-w-md flex-1 flex-col items-center justify-center rounded border-none bg-transparent shadow-none">
101+
<CardContent className="flex flex-col items-center justify-center text-center px-6 sm:px-8 lg:px-12 py-12 sm:py-14">
102+
<div
103+
aria-hidden="true"
104+
className="flex size-12 items-center justify-center rounded-2xl bg-destructive/10"
105+
role="img"
106+
>
107+
<WarningCircleIcon
108+
aria-hidden="true"
109+
className="size-6 text-destructive"
110+
size={24}
111+
weight="fill"
112+
/>
113+
</div>
114+
115+
<div className="mt-6 space-y-4 max-w-sm w-full">
116+
<h1 className="font-semibold text-foreground text-lg">
117+
Something Went Wrong
118+
</h1>
119+
<p className="text-muted-foreground text-sm leading-relaxed text-balance">
120+
We encountered an unexpected issue. Please try again.
121+
</p>
122+
{error?.message && (
123+
<div className="mx-auto mt-2 w-full rounded-md bg-destructive/10 border border-destructive/20 p-2">
124+
<p className="text-destructive text-xs font-mono wrap-break-word">
125+
{error.message}
126+
</p>
127+
</div>
128+
)}
129+
</div>
130+
131+
<Button
132+
className="mt-6 w-full max-w-xs"
133+
onClick={() => setOpen(true)}
134+
variant="outline"
135+
>
136+
<MagnifyingGlassIcon className="mr-2 size-4" weight="duotone" />
137+
Search pages, settings...
138+
<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">
139+
<CommandIcon className="size-3" weight="bold" />
140+
<span>K</span>
141+
</kbd>
142+
</Button>
143+
144+
<div className="mt-6 flex w-full max-w-xs flex-col items-stretch justify-center gap-3 sm:flex-row sm:items-center">
145+
{canGoBack && (
146+
<Button
147+
className="flex-1"
148+
onClick={() => router.back()}
149+
variant="outline"
150+
>
151+
<ArrowLeftIcon className="mr-2 size-4" weight="duotone" />
152+
Go Back
153+
</Button>
154+
)}
155+
<Button
156+
className={canGoBack ? "flex-1 bg-primary hover:bg-primary/90" : "w-full bg-primary hover:bg-primary/90"}
157+
onClick={() => reset()}
158+
variant="default"
159+
>
160+
Try Again
161+
</Button>
162+
<Button
163+
asChild
164+
className={canGoBack ? "flex-1" : "w-full"}
165+
variant="outline"
166+
>
167+
<a href="/">
168+
<HouseIcon className="mr-2 size-4" weight="duotone" />
169+
Home
170+
</a>
171+
</Button>
172+
</div>
173+
174+
<Dialog onOpenChange={setOpen} open={open}>
175+
<DialogHeader className="sr-only">
176+
<DialogTitle>Search</DialogTitle>
177+
<DialogDescription>Search for pages and settings</DialogDescription>
178+
</DialogHeader>
179+
<DialogContent
180+
className="gap-0 overflow-hidden p-0 sm:max-w-xl"
181+
showCloseButton={false}
182+
>
183+
<CommandPrimitive
184+
className="flex h-full w-full flex-col"
185+
loop
186+
onKeyDown={(e) => {
187+
if (e.key === "Escape") {
188+
setOpen(false);
189+
}
190+
}}
191+
>
192+
<div className="dotted-bg flex items-center gap-3 border-b bg-accent px-4 py-3">
193+
<div className="flex size-8 shrink-0 items-center justify-center rounded bg-background">
194+
<MagnifyingGlassIcon
195+
className="size-4 text-muted-foreground"
196+
weight="duotone"
197+
/>
198+
</div>
199+
<CommandPrimitive.Input
200+
className="h-8 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
201+
onValueChange={setSearch}
202+
placeholder="Search pages, settings..."
203+
value={search}
204+
/>
205+
<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">
206+
<CommandIcon className="size-3" weight="bold" />
207+
<span>K</span>
208+
</kbd>
209+
</div>
210+
211+
<CommandPrimitive.List className="max-h-80 overflow-y-auto scroll-py-2 p-2">
212+
<CommandPrimitive.Empty className="flex flex-col items-center justify-center gap-2 py-12 text-center">
213+
<MagnifyingGlassIcon
214+
className="size-8 text-muted-foreground/50"
215+
weight="duotone"
216+
/>
217+
<div>
218+
<p className="font-medium text-muted-foreground text-sm">No results found</p>
219+
<p className="text-muted-foreground/70 text-xs">
220+
Try searching for something else
221+
</p>
222+
</div>
223+
</CommandPrimitive.Empty>
224+
{searchItems.map((item) => {
225+
const ItemIcon = item.icon;
226+
return (
227+
<CommandPrimitive.Item
228+
className={cn(
229+
"group relative flex cursor-pointer select-none items-center gap-3 rounded px-2 py-2 outline-none transition-colors",
230+
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground"
231+
)}
232+
key={item.path}
233+
onSelect={() => handleSelect(item)}
234+
value={`${item.name} ${item.path}`}
235+
>
236+
<div className="flex size-7 shrink-0 items-center justify-center rounded bg-accent transition-colors group-data-[selected=true]:bg-background">
237+
<ItemIcon className="size-4 text-muted-foreground" weight="duotone" />
238+
</div>
239+
<div className="min-w-0 flex-1">
240+
<p className="truncate font-medium text-sm leading-tight">{item.name}</p>
241+
<p className="truncate text-muted-foreground text-xs">{item.path}</p>
242+
</div>
243+
</CommandPrimitive.Item>
244+
);
245+
})}
246+
</CommandPrimitive.List>
247+
</CommandPrimitive>
248+
</DialogContent>
249+
</Dialog>
250+
</CardContent>
251+
</Card>
51252
</div>
52253
);
53254
}

0 commit comments

Comments
 (0)