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