The recipes.blue monorepo recipes.blue
recipes appview atproto

feat: better error handling with wrapper route

+299 -139
+9 -1
apps/web/src/queries/recipe.ts
··· 1 1 import { useXrpc } from "@/hooks/use-xrpc"; 2 - import { XRPC } from "@atcute/client"; 2 + import { XRPC, XRPCError } from "@atcute/client"; 3 3 import { queryOptions, useQuery } from "@tanstack/react-query"; 4 + import { notFound } from "@tanstack/react-router"; 4 5 5 6 const RQKEY_ROOT = 'posts'; 6 7 export const RQKEY = (cursor: string, did: string, rkey: string) => [RQKEY_ROOT, cursor, did, rkey]; ··· 22 23 return queryOptions({ 23 24 queryKey: RQKEY('', did, rkey), 24 25 queryFn: async () => { 26 + try { 25 27 const res = await rpc.get('moe.hayden.cookware.getRecipe', { 26 28 params: { did, rkey }, 27 29 }); 28 30 return res.data; 31 + } catch (err) { 32 + if (err instanceof XRPCError && err.kind && err.kind == 'not_found') { 33 + throw notFound({ routeId: '/_' }); 34 + } 35 + throw err; 36 + } 29 37 }, 30 38 }); 31 39 };
+73 -34
apps/web/src/routeTree.gen.ts
··· 13 13 // Import Routes 14 14 15 15 import { Route as rootRoute } from './routes/__root' 16 - import { Route as appRecipesAuthorRkeyImport } from './routes/(app)/recipes/$author/$rkey' 16 + import { Route as Import } from './routes/_' 17 + import { Route as appRecipesAuthorRkeyImport } from './routes/_.(app)/recipes/$author/$rkey' 17 18 18 19 // Create Virtual Routes 19 20 20 - const appIndexLazyImport = createFileRoute('/(app)/')() 21 - const authLoginLazyImport = createFileRoute('/(auth)/login')() 21 + const appIndexLazyImport = createFileRoute('/_/(app)/')() 22 + const authLoginLazyImport = createFileRoute('/_/(auth)/login')() 22 23 23 24 // Create/Update Routes 24 25 26 + const Route = Import.update({ 27 + id: '/_', 28 + getParentRoute: () => rootRoute, 29 + } as any) 30 + 25 31 const appIndexLazyRoute = appIndexLazyImport 26 32 .update({ 27 33 id: '/(app)/', 28 34 path: '/', 29 - getParentRoute: () => rootRoute, 35 + getParentRoute: () => Route, 30 36 } as any) 31 - .lazy(() => import('./routes/(app)/index.lazy').then((d) => d.Route)) 37 + .lazy(() => import('./routes/_.(app)/index.lazy').then((d) => d.Route)) 32 38 33 39 const authLoginLazyRoute = authLoginLazyImport 34 40 .update({ 35 41 id: '/(auth)/login', 36 42 path: '/login', 37 - getParentRoute: () => rootRoute, 43 + getParentRoute: () => Route, 38 44 } as any) 39 - .lazy(() => import('./routes/(auth)/login.lazy').then((d) => d.Route)) 45 + .lazy(() => import('./routes/_.(auth)/login.lazy').then((d) => d.Route)) 40 46 41 47 const appRecipesAuthorRkeyRoute = appRecipesAuthorRkeyImport.update({ 42 48 id: '/(app)/recipes/$author/$rkey', 43 49 path: '/recipes/$author/$rkey', 44 - getParentRoute: () => rootRoute, 50 + getParentRoute: () => Route, 45 51 } as any) 46 52 47 53 // Populate the FileRoutesByPath interface 48 54 49 55 declare module '@tanstack/react-router' { 50 56 interface FileRoutesByPath { 51 - '/(auth)/login': { 52 - id: '/(auth)/login' 57 + '/_': { 58 + id: '/_' 59 + path: '' 60 + fullPath: '' 61 + preLoaderRoute: typeof Import 62 + parentRoute: typeof rootRoute 63 + } 64 + '/_/(auth)/login': { 65 + id: '/_/(auth)/login' 53 66 path: '/login' 54 67 fullPath: '/login' 55 68 preLoaderRoute: typeof authLoginLazyImport 56 69 parentRoute: typeof rootRoute 57 70 } 58 - '/(app)/': { 59 - id: '/(app)/' 71 + '/_/(app)/': { 72 + id: '/_/(app)/' 60 73 path: '/' 61 74 fullPath: '/' 62 75 preLoaderRoute: typeof appIndexLazyImport 63 76 parentRoute: typeof rootRoute 64 77 } 65 - '/(app)/recipes/$author/$rkey': { 66 - id: '/(app)/recipes/$author/$rkey' 78 + '/_/(app)/recipes/$author/$rkey': { 79 + id: '/_/(app)/recipes/$author/$rkey' 67 80 path: '/recipes/$author/$rkey' 68 81 fullPath: '/recipes/$author/$rkey' 69 82 preLoaderRoute: typeof appRecipesAuthorRkeyImport ··· 74 87 75 88 // Create and export the route tree 76 89 90 + interface RouteChildren { 91 + authLoginLazyRoute: typeof authLoginLazyRoute 92 + appIndexLazyRoute: typeof appIndexLazyRoute 93 + appRecipesAuthorRkeyRoute: typeof appRecipesAuthorRkeyRoute 94 + } 95 + 96 + const RouteChildren: RouteChildren = { 97 + authLoginLazyRoute: authLoginLazyRoute, 98 + appIndexLazyRoute: appIndexLazyRoute, 99 + appRecipesAuthorRkeyRoute: appRecipesAuthorRkeyRoute, 100 + } 101 + 102 + const RouteWithChildren = Route._addFileChildren(RouteChildren) 103 + 77 104 export interface FileRoutesByFullPath { 105 + '': typeof RouteWithChildren 78 106 '/login': typeof authLoginLazyRoute 79 107 '/': typeof appIndexLazyRoute 80 108 '/recipes/$author/$rkey': typeof appRecipesAuthorRkeyRoute ··· 88 116 89 117 export interface FileRoutesById { 90 118 __root__: typeof rootRoute 91 - '/(auth)/login': typeof authLoginLazyRoute 92 - '/(app)/': typeof appIndexLazyRoute 93 - '/(app)/recipes/$author/$rkey': typeof appRecipesAuthorRkeyRoute 119 + '/_': typeof RouteWithChildren 120 + '/_/(auth)/login': typeof authLoginLazyRoute 121 + '/_/(app)/': typeof appIndexLazyRoute 122 + '/_/(app)/recipes/$author/$rkey': typeof appRecipesAuthorRkeyRoute 94 123 } 95 124 96 125 export interface FileRouteTypes { 97 126 fileRoutesByFullPath: FileRoutesByFullPath 98 - fullPaths: '/login' | '/' | '/recipes/$author/$rkey' 127 + fullPaths: '' | '/login' | '/' | '/recipes/$author/$rkey' 99 128 fileRoutesByTo: FileRoutesByTo 100 129 to: '/login' | '/' | '/recipes/$author/$rkey' 101 - id: '__root__' | '/(auth)/login' | '/(app)/' | '/(app)/recipes/$author/$rkey' 130 + id: 131 + | '__root__' 132 + | '/_' 133 + | '/_/(auth)/login' 134 + | '/_/(app)/' 135 + | '/_/(app)/recipes/$author/$rkey' 102 136 fileRoutesById: FileRoutesById 103 137 } 104 138 105 139 export interface RootRouteChildren { 106 - authLoginLazyRoute: typeof authLoginLazyRoute 107 - appIndexLazyRoute: typeof appIndexLazyRoute 108 - appRecipesAuthorRkeyRoute: typeof appRecipesAuthorRkeyRoute 140 + Route: typeof RouteWithChildren 109 141 } 110 142 111 143 const rootRouteChildren: RootRouteChildren = { 112 - authLoginLazyRoute: authLoginLazyRoute, 113 - appIndexLazyRoute: appIndexLazyRoute, 114 - appRecipesAuthorRkeyRoute: appRecipesAuthorRkeyRoute, 144 + Route: RouteWithChildren, 115 145 } 116 146 117 147 export const routeTree = rootRoute ··· 124 154 "__root__": { 125 155 "filePath": "__root.tsx", 126 156 "children": [ 127 - "/(auth)/login", 128 - "/(app)/", 129 - "/(app)/recipes/$author/$rkey" 157 + "/_" 130 158 ] 131 159 }, 132 - "/(auth)/login": { 133 - "filePath": "(auth)/login.lazy.tsx" 160 + "/_": { 161 + "filePath": "_.tsx", 162 + "children": [ 163 + "/_/(auth)/login", 164 + "/_/(app)/", 165 + "/_/(app)/recipes/$author/$rkey" 166 + ] 134 167 }, 135 - "/(app)/": { 136 - "filePath": "(app)/index.lazy.tsx" 168 + "/_/(auth)/login": { 169 + "filePath": "_.(auth)/login.lazy.tsx", 170 + "parent": "/_" 171 + }, 172 + "/_/(app)/": { 173 + "filePath": "_.(app)/index.lazy.tsx", 174 + "parent": "/_" 137 175 }, 138 - "/(app)/recipes/$author/$rkey": { 139 - "filePath": "(app)/recipes/$author/$rkey.tsx" 176 + "/_/(app)/recipes/$author/$rkey": { 177 + "filePath": "_.(app)/recipes/$author/$rkey.tsx", 178 + "parent": "/_" 140 179 } 141 180 } 142 181 }
+6 -4
apps/web/src/routes/(app)/index.lazy.tsx apps/web/src/routes/_.(app)/index.lazy.tsx
··· 13 13 import { useRecipesQuery } from '@/queries/recipe' 14 14 import { RecipeCard } from '@/screens/Recipes/RecipeCard' 15 15 16 - export const Route = createLazyFileRoute('/(app)/')({ 16 + export const Route = createLazyFileRoute('/_/(app)/')({ 17 17 component: RouteComponent, 18 18 }) 19 19 20 20 function RouteComponent() { 21 - const query = useRecipesQuery(''); 21 + const query = useRecipesQuery('') 22 22 23 23 return ( 24 24 <> ··· 29 29 <Breadcrumb> 30 30 <BreadcrumbList> 31 31 <BreadcrumbItem className="hidden md:block"> 32 - <BreadcrumbLink asChild><Link href="/">Community</Link></BreadcrumbLink> 32 + <BreadcrumbLink asChild> 33 + <Link href="/">Community</Link> 34 + </BreadcrumbLink> 33 35 </BreadcrumbItem> 34 36 <BreadcrumbSeparator className="hidden md:block" /> 35 37 <BreadcrumbItem> ··· 52 54 steps={recipe.steps} 53 55 ingredients={recipe.ingredients} 54 56 key={idx} 55 - /> 57 + /> 56 58 ))} 57 59 </QueryPlaceholder> 58 60 </div>
+27 -11
apps/web/src/routes/(app)/recipes/$author/$rkey.tsx apps/web/src/routes/_.(app)/recipes/$author/$rkey.tsx
··· 15 15 import { useSuspenseQuery } from '@tanstack/react-query' 16 16 import { rpc } from '@/hooks/use-xrpc' 17 17 18 - export const Route = createFileRoute('/(app)/recipes/$author/$rkey')({ 19 - loader: ({ params: { author, rkey }, }) => { 20 - queryClient.ensureQueryData(recipeQueryOptions(rpc, author, rkey)); 18 + export const Route = createFileRoute('/_/(app)/recipes/$author/$rkey')({ 19 + loader: ({ params: { author, rkey } }) => { 20 + queryClient.ensureQueryData(recipeQueryOptions(rpc, author, rkey)) 21 21 }, 22 - 23 22 component: RouteComponent, 24 23 }) 25 24 ··· 27 26 const { author, rkey } = Route.useParams() 28 27 const { 29 28 data: { recipe }, 30 - } = useSuspenseQuery(recipeQueryOptions(rpc, author, rkey)); 29 + error, 30 + } = useSuspenseQuery(recipeQueryOptions(rpc, author, rkey)) 31 + 32 + if (error) return <p>Error</p> 31 33 32 34 return ( 33 35 <> ··· 38 40 <Breadcrumb> 39 41 <BreadcrumbList> 40 42 <BreadcrumbItem className="hidden md:block"> 41 - <BreadcrumbLink asChild><Link to="/">Community</Link></BreadcrumbLink> 43 + <BreadcrumbLink asChild> 44 + <Link to="/">Community</Link> 45 + </BreadcrumbLink> 42 46 </BreadcrumbItem> 43 47 <BreadcrumbSeparator className="hidden md:block" /> 44 48 <BreadcrumbItem className="hidden md:block"> 45 - <BreadcrumbLink asChild><Link to="/">Browse Recipes</Link></BreadcrumbLink> 49 + <BreadcrumbLink asChild> 50 + <Link to="/">Browse Recipes</Link> 51 + </BreadcrumbLink> 46 52 </BreadcrumbItem> 47 53 <BreadcrumbSeparator className="hidden md:block" /> 48 54 <BreadcrumbItem className="hidden md:block"> 49 - <BreadcrumbLink asChild><Link href={`/profiles/${recipe.author.handle}`}>{recipe.author.handle}</Link></BreadcrumbLink> 55 + <BreadcrumbLink asChild> 56 + <Link href={`/profiles/${recipe.author.handle}`}> 57 + {recipe.author.handle} 58 + </Link> 59 + </BreadcrumbLink> 50 60 </BreadcrumbItem> 51 61 <BreadcrumbSeparator className="hidden md:block" /> 52 62 <BreadcrumbItem> ··· 58 68 </header> 59 69 <div className="flex flex-1 flex-col gap-4 p-4 pt-0"> 60 70 <div className="max-w-6xl"> 61 - <h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">{recipe.title}</h1> 62 - <p className="leading-7 [&:not(:first-child)]:mt-6">{recipe.description}</p> 71 + <h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl"> 72 + {recipe.title} 73 + </h1> 74 + <p className="leading-7 [&:not(:first-child)]:mt-6"> 75 + {recipe.description} 76 + </p> 63 77 </div> 64 78 65 79 <div className="grid lg:grid-cols-3 gap-4"> ··· 70 84 <CardContent> 71 85 <ul> 72 86 {recipe.ingredients.map((ing, idx) => ( 73 - <li key={idx}>{ing.name} ({ing.amount} {ing.unit})</li> 87 + <li key={idx}> 88 + {ing.name} ({ing.amount} {ing.unit}) 89 + </li> 74 90 ))} 75 91 </ul> 76 92 </CardContent>
-85
apps/web/src/routes/(auth)/login.lazy.tsx
··· 1 - import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage } from '@/components/ui/breadcrumb'; 2 - import { Button } from '@/components/ui/button'; 3 - import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; 4 - import { Input } from '@/components/ui/input'; 5 - import { Label } from '@/components/ui/label'; 6 - import { Separator } from '@/components/ui/separator'; 7 - import { SidebarTrigger } from '@/components/ui/sidebar'; 8 - import { SERVER_URL } from '@/lib/utils'; 9 - import { useMutation } from '@tanstack/react-query'; 10 - import { createLazyFileRoute } from '@tanstack/react-router' 11 - import { useState } from 'react'; 12 - 13 - export const Route = createLazyFileRoute('/(auth)/login')({ 14 - component: RouteComponent, 15 - }) 16 - 17 - function RouteComponent() { 18 - const [handle, setHandle] = useState(''); 19 - 20 - const { mutate, isPending, error } = useMutation({ 21 - mutationKey: ['login'], 22 - mutationFn: async () => { 23 - const res = await fetch(`https://${SERVER_URL}/oauth/login`, { 24 - method: 'POST', 25 - body: JSON.stringify({ actor: handle }), 26 - redirect: 'manual', 27 - headers: { 28 - 'Content-Type': 'application/json', 29 - 'Accept': 'application/json', 30 - }, 31 - }); 32 - return res.json() 33 - }, 34 - onSuccess: (resp: { url: string }) => { 35 - document.location.href = resp.url; 36 - }, 37 - }); 38 - 39 - return ( 40 - <> 41 - <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12"> 42 - <div className="flex items-center gap-2 px-4"> 43 - <SidebarTrigger className="-ml-1" /> 44 - <Separator orientation="vertical" className="mr-2 h-4" /> 45 - <Breadcrumb> 46 - <BreadcrumbList> 47 - <BreadcrumbItem> 48 - <BreadcrumbPage>Log in</BreadcrumbPage> 49 - </BreadcrumbItem> 50 - </BreadcrumbList> 51 - </Breadcrumb> 52 - </div> 53 - </header> 54 - <div className="flex flex-1 flex-col items-center justify-center gap-4 p-4 pt-0"> 55 - <Card className="max-w-xl w-full"> 56 - <CardHeader> 57 - <CardTitle>Log in</CardTitle> 58 - <CardDescription> 59 - Don't have an account? <a className="font-bold text-primary" href="https://bsky.app/" target="_blank">Sign up on Bluesky!</a> 60 - </CardDescription> 61 - </CardHeader> 62 - <CardContent> 63 - <div className="flex flex-col gap-2"> 64 - <Label htmlFor="handle">Handle</Label> 65 - <Input 66 - className={`${error ? 'border-destructive text-destructive' : ''}`} 67 - type="text" 68 - id="handle" 69 - name="handle" 70 - placeholder="johndoe.bsky.social" 71 - required 72 - value={handle} 73 - onChange={e => setHandle(e.currentTarget.value)} 74 - /> 75 - {error && <p className="text-sm font-medium text-destructive">{error.message}</p>} 76 - </div> 77 - </CardContent> 78 - <CardFooter> 79 - <Button onClick={() => mutate()} disabled={isPending}>Log in</Button> 80 - </CardFooter> 81 - </Card> 82 - </div> 83 - </> 84 - ); 85 - }
+113
apps/web/src/routes/_.(auth)/login.lazy.tsx
··· 1 + import { 2 + Breadcrumb, 3 + BreadcrumbItem, 4 + BreadcrumbList, 5 + BreadcrumbPage, 6 + } from '@/components/ui/breadcrumb' 7 + import { Button } from '@/components/ui/button' 8 + import { 9 + Card, 10 + CardContent, 11 + CardDescription, 12 + CardFooter, 13 + CardHeader, 14 + CardTitle, 15 + } from '@/components/ui/card' 16 + import { Input } from '@/components/ui/input' 17 + import { Label } from '@/components/ui/label' 18 + import { Separator } from '@/components/ui/separator' 19 + import { SidebarTrigger } from '@/components/ui/sidebar' 20 + import { SERVER_URL } from '@/lib/utils' 21 + import { useMutation } from '@tanstack/react-query' 22 + import { createLazyFileRoute } from '@tanstack/react-router' 23 + import { useState } from 'react' 24 + 25 + export const Route = createLazyFileRoute('/_/(auth)/login')({ 26 + component: RouteComponent, 27 + }) 28 + 29 + function RouteComponent() { 30 + const [handle, setHandle] = useState('') 31 + 32 + const { mutate, isPending, error } = useMutation({ 33 + mutationKey: ['login'], 34 + mutationFn: async () => { 35 + const res = await fetch(`https://${SERVER_URL}/oauth/login`, { 36 + method: 'POST', 37 + body: JSON.stringify({ actor: handle }), 38 + redirect: 'manual', 39 + headers: { 40 + 'Content-Type': 'application/json', 41 + Accept: 'application/json', 42 + }, 43 + }) 44 + return res.json() 45 + }, 46 + onSuccess: (resp: { url: string }) => { 47 + document.location.href = resp.url 48 + }, 49 + }) 50 + 51 + return ( 52 + <> 53 + <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12"> 54 + <div className="flex items-center gap-2 px-4"> 55 + <SidebarTrigger className="-ml-1" /> 56 + <Separator orientation="vertical" className="mr-2 h-4" /> 57 + <Breadcrumb> 58 + <BreadcrumbList> 59 + <BreadcrumbItem> 60 + <BreadcrumbPage>Log in</BreadcrumbPage> 61 + </BreadcrumbItem> 62 + </BreadcrumbList> 63 + </Breadcrumb> 64 + </div> 65 + </header> 66 + <div className="flex flex-1 flex-col items-center justify-center gap-4 p-4 pt-0"> 67 + <Card className="max-w-sm w-full"> 68 + <CardHeader> 69 + <CardTitle>Log in</CardTitle> 70 + <CardDescription> 71 + Enter your handle below to sign in to your account. 72 + </CardDescription> 73 + </CardHeader> 74 + <CardContent> 75 + <div className="flex flex-col gap-2"> 76 + <Label htmlFor="handle">Handle</Label> 77 + <Input 78 + className={`${error ? 'border-destructive text-destructive' : ''}`} 79 + type="text" 80 + id="handle" 81 + name="handle" 82 + placeholder="johndoe.bsky.social" 83 + required 84 + value={handle} 85 + onChange={(e) => setHandle(e.currentTarget.value)} 86 + /> 87 + {error && ( 88 + <p className="text-sm font-medium text-destructive"> 89 + {error.message} 90 + </p> 91 + )} 92 + </div> 93 + </CardContent> 94 + <CardFooter className="grid gap-2"> 95 + <Button onClick={() => mutate()} disabled={isPending}> 96 + Log in 97 + </Button> 98 + <p className="text-sm text-muted-foreground text-center"> 99 + Don't have an account?{' '} 100 + <a 101 + className="font-bold text-primary" 102 + href="https://bsky.app/" 103 + target="_blank" 104 + > 105 + Sign up on Bluesky! 106 + </a> 107 + </p> 108 + </CardFooter> 109 + </Card> 110 + </div> 111 + </> 112 + ) 113 + }
+70
apps/web/src/routes/_.tsx
··· 1 + import { Link, createFileRoute, Outlet } from '@tanstack/react-router' 2 + import { Button } from '@/components/ui/button' 3 + import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' 4 + import { SidebarTrigger } from '@/components/ui/sidebar' 5 + 6 + export const Route = createFileRoute('/_')({ 7 + component: RouteComponent, 8 + errorComponent: ({ error }) => { 9 + return ( 10 + <> 11 + <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12"> 12 + <div className="flex items-center gap-2 px-4"> 13 + <SidebarTrigger className="-ml-1" /> 14 + </div> 15 + </header> 16 + <div className="flex flex-1 flex-col gap-4 p-4 pt-0"> 17 + <Card className="m-auto max-w-sm"> 18 + <CardHeader> 19 + <CardTitle>Error!</CardTitle> 20 + </CardHeader> 21 + <CardContent> 22 + {error.message} 23 + </CardContent> 24 + <CardFooter> 25 + <Button asChild> 26 + <Link to="/">Go home</Link> 27 + </Button> 28 + </CardFooter> 29 + </Card> 30 + </div> 31 + </> 32 + ); 33 + }, 34 + 35 + notFoundComponent: () => { 36 + return ( 37 + <> 38 + <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12"> 39 + <div className="flex items-center gap-2 px-4"> 40 + <SidebarTrigger className="-ml-1" /> 41 + </div> 42 + </header> 43 + <div className="flex flex-1 flex-col gap-4 p-4 pt-0"> 44 + <Card className="m-auto max-w-sm"> 45 + <CardHeader> 46 + <CardTitle>Not found</CardTitle> 47 + </CardHeader> 48 + <CardContent> 49 + {"The page you tried to view doesn't exist."} 50 + </CardContent> 51 + <CardFooter> 52 + <Button asChild> 53 + <Link to="/">Go home</Link> 54 + </Button> 55 + </CardFooter> 56 + </Card> 57 + </div> 58 + </> 59 + ); 60 + }, 61 + 62 + }) 63 + 64 + function RouteComponent() { 65 + return ( 66 + <> 67 + <Outlet /> 68 + </> 69 + ) 70 + }
+1 -4
apps/web/src/routes/__root.tsx
··· 4 4 SidebarProvider, 5 5 } from '@/components/ui/sidebar' 6 6 import { Outlet, createRootRoute } from '@tanstack/react-router' 7 - import { Suspense } from 'react' 8 7 9 8 export const Route = createRootRoute({ 10 9 component: RootComponent, 11 - }) 10 + }); 12 11 13 12 function RootComponent() { 14 13 return ( 15 14 <SidebarProvider> 16 15 <AppSidebar /> 17 16 <SidebarInset> 18 - <Suspense fallback={<p>Loading...</p>}> 19 17 <Outlet /> 20 - </Suspense> 21 18 </SidebarInset> 22 19 </SidebarProvider> 23 20 )