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" ;
514import { 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
762export 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