A personal media tracker built on the AT Protocol opnshelf.xyz
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add custom error screens (404 and error boundary) with mobile menu overlay

+135 -5
+125
apps/web/src/components/ErrorBoundary.tsx
··· 1 + import { Link, useRouter } from "@tanstack/react-router"; 2 + import { AlertTriangle, ArrowLeft, Home, RefreshCw } from "lucide-react"; 3 + 4 + interface ErrorComponentProps { 5 + error?: Error; 6 + reset?: () => void; 7 + } 8 + 9 + export function DefaultErrorComponent({ error, reset }: ErrorComponentProps) { 10 + const router = useRouter(); 11 + 12 + const handleRetry = () => { 13 + if (reset) { 14 + reset(); 15 + } else { 16 + router.invalidate(); 17 + } 18 + }; 19 + 20 + return ( 21 + <div className="container-app flex min-h-[60vh] flex-col items-center justify-center py-16 text-center"> 22 + {/* Error Icon */} 23 + <div className="mb-6 flex h-20 w-20 items-center justify-center rounded-2xl bg-red-50 dark:bg-red-950/20"> 24 + <AlertTriangle className="h-10 w-10 text-red-500" /> 25 + </div> 26 + 27 + {/* Error Title */} 28 + <h1 className="text-display-2 mb-3 text-[var(--foreground)]"> 29 + Something went wrong 30 + </h1> 31 + 32 + {/* Error Description */} 33 + <p className="mb-8 max-w-md text-lg text-[var(--foreground-muted)]"> 34 + {error?.message || 35 + "We encountered an unexpected error. Please try again or go back to the home page."} 36 + </p> 37 + 38 + {/* Error Details (only in development) */} 39 + {import.meta.env.DEV && error?.stack && ( 40 + <div className="mb-8 max-w-2xl overflow-auto rounded-lg border border-red-200 bg-red-50 p-4 text-left dark:border-red-900 dark:bg-red-950/20"> 41 + <p className="mb-2 text-sm font-semibold text-red-700 dark:text-red-400"> 42 + Stack trace: 43 + </p> 44 + <pre className="text-xs text-red-600 dark:text-red-300"> 45 + {error.stack} 46 + </pre> 47 + </div> 48 + )} 49 + 50 + {/* Action Buttons */} 51 + <div className="flex flex-col gap-3 sm:flex-row sm:items-center"> 52 + <button 53 + type="button" 54 + onClick={handleRetry} 55 + className="inline-flex items-center justify-center gap-2 rounded-md bg-[var(--accent)] px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-[var(--accent-hover)]" 56 + > 57 + <RefreshCw className="h-4 w-4" /> 58 + Try Again 59 + </button> 60 + 61 + <Link 62 + to="/" 63 + className="inline-flex items-center justify-center gap-2 rounded-md border border-[var(--border)] bg-[var(--background-elevated)] px-6 py-3 text-sm font-medium text-[var(--foreground)] transition-colors hover:bg-[var(--background-subtle)] hover:border-[var(--border-strong)]" 64 + > 65 + <Home className="h-4 w-4" /> 66 + Go Home 67 + </Link> 68 + 69 + <button 70 + type="button" 71 + onClick={() => router.history.back()} 72 + className="inline-flex items-center justify-center gap-2 rounded-md border border-transparent px-6 py-3 text-sm font-medium text-[var(--foreground-muted)] transition-colors hover:bg-[var(--background-subtle)] hover:text-[var(--foreground)]" 73 + > 74 + <ArrowLeft className="h-4 w-4" /> 75 + Go Back 76 + </button> 77 + </div> 78 + </div> 79 + ); 80 + } 81 + 82 + export function NotFoundComponent() { 83 + const router = useRouter(); 84 + 85 + return ( 86 + <div className="container-app flex min-h-[60vh] flex-col items-center justify-center py-16 text-center"> 87 + {/* 404 Icon */} 88 + <div className="mb-6 flex h-20 w-20 items-center justify-center rounded-2xl bg-[var(--accent-subtle)]"> 89 + <span className="text-4xl font-bold text-[var(--accent)]">404</span> 90 + </div> 91 + 92 + {/* Title */} 93 + <h1 className="text-display-2 mb-3 text-[var(--foreground)]"> 94 + Page not found 95 + </h1> 96 + 97 + {/* Description */} 98 + <p className="mb-8 max-w-md text-lg text-[var(--foreground-muted)]"> 99 + The page you&apos;re looking for doesn&apos;t exist or has been moved. 100 + </p> 101 + 102 + {/* Action Buttons */} 103 + <div className="flex flex-col gap-3 sm:flex-row sm:items-center"> 104 + <Link 105 + to="/" 106 + className="inline-flex items-center justify-center gap-2 rounded-md bg-[var(--accent)] px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-[var(--accent-hover)]" 107 + > 108 + <Home className="h-4 w-4" /> 109 + Go Home 110 + </Link> 111 + 112 + <button 113 + type="button" 114 + onClick={() => router.history.back()} 115 + className="inline-flex items-center justify-center gap-2 rounded-md border border-[var(--border)] bg-[var(--background-elevated)] px-6 py-3 text-sm font-medium text-[var(--foreground)] transition-colors hover:bg-[var(--background-subtle)] hover:border-[var(--border-strong)]" 116 + > 117 + <ArrowLeft className="h-4 w-4" /> 118 + Go Back 119 + </button> 120 + </div> 121 + </div> 122 + ); 123 + } 124 + 125 + export default DefaultErrorComponent;
+7 -5
apps/web/src/components/Header.tsx
··· 206 206 </div> 207 207 </nav> 208 208 209 - {/* Mobile Navigation */} 209 + {/* Mobile Navigation - Overlay */} 210 210 {mobileMenuOpen && ( 211 - <div className="animate-slide-up border-t border-[var(--border)] py-4 md:hidden"> 212 - <div className="flex flex-col gap-1"> 211 + <div className="fixed inset-x-0 top-16 z-40 h-[calc(100vh-4rem)] border-t border-[var(--border)] bg-[var(--background)] md:hidden"> 212 + <div className="container-app h-full overflow-y-auto py-4"> 213 + <div className="flex flex-col gap-1"> 213 214 {navigation.map((item) => { 214 215 const isActive = 215 216 currentPath === item.href || ··· 280 281 )} 281 282 </div> 282 283 </div> 283 - )} 284 - </div> 284 + </div> 285 + )} 286 + </div> 285 287 </header> 286 288 ); 287 289 }
+3
apps/web/src/routes/__root.tsx
··· 6 6 Scripts, 7 7 } from "@tanstack/react-router"; 8 8 import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"; 9 + import { DefaultErrorComponent, NotFoundComponent } from "../components/ErrorBoundary"; 9 10 import Footer from "../components/Footer"; 10 11 import Header from "../components/Header"; 11 12 import PostHogProvider from "../integrations/posthog/provider"; ··· 33 34 ], 34 35 links: [{ rel: "stylesheet", href: appCss }], 35 36 }), 37 + errorComponent: DefaultErrorComponent, 38 + notFoundComponent: NotFoundComponent, 36 39 shellComponent: RootDocument, 37 40 }); 38 41