create-croissant 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/apps/web/src/components/app-sidebar.tsx +43 -28
- package/template/apps/web/src/components/login-form.tsx +1 -1
- package/template/apps/web/src/components/signup-form.tsx +1 -1
- package/template/apps/web/src/routeTree.gen.ts +21 -0
- package/template/apps/web/src/routes/account.tsx +263 -0
- package/template/apps/web/src/routes/client-orpc-auth.tsx +2 -2
- package/template/apps/web/src/routes/client-orpc.tsx +16 -10
- package/template/apps/web/src/routes/dashboard.tsx +3 -3
- package/template/apps/web/src/routes/index.tsx +8 -3
- package/template/apps/web/src/routes/isr.tsx +2 -2
- package/template/apps/web/src/routes/login.tsx +1 -1
- package/template/apps/web/src/routes/signup.tsx +1 -1
- package/template/apps/web/src/routes/ssr-orpc-auth.tsx +2 -2
- package/template/apps/web/src/routes/ssr-orpc.tsx +11 -6
- package/template/packages/orpc/src/lib/planets.ts +79 -0
- package/template/packages/orpc/src/lib/router.ts +3 -72
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
2
|
import { Link } from "@tanstack/react-router"
|
|
3
|
-
import { LogOut, User } from "lucide-react"
|
|
3
|
+
import { LogOut, Settings, User } from "lucide-react"
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
Sidebar,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
SidebarRail,
|
|
17
17
|
} from "@workspace/ui/components/sidebar"
|
|
18
18
|
import { Avatar, AvatarFallback, AvatarImage } from "@workspace/ui/components/avatar"
|
|
19
|
-
import { authClient } from "
|
|
19
|
+
import { authClient } from "@/lib/auth-client"
|
|
20
20
|
|
|
21
21
|
// This is sample data.
|
|
22
22
|
const data = {
|
|
@@ -50,7 +50,7 @@ const data = {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|
53
|
-
const [user, setUser] = React.useState<
|
|
53
|
+
const [user, setUser] = React.useState<typeof authClient.$Infer.Session.user | null>(null)
|
|
54
54
|
|
|
55
55
|
React.useEffect(() => {
|
|
56
56
|
const checkSession = async () => {
|
|
@@ -95,32 +95,47 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|
|
95
95
|
<SidebarFooter>
|
|
96
96
|
<SidebarMenu>
|
|
97
97
|
<SidebarMenuItem>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
<Avatar className="h-8 w-8 rounded-lg">
|
|
104
|
-
<AvatarImage src={user.image || ""} alt={user.name} />
|
|
105
|
-
<AvatarFallback className="rounded-lg">
|
|
106
|
-
{user.name?.charAt(0) || "U"}
|
|
107
|
-
</AvatarFallback>
|
|
108
|
-
</Avatar>
|
|
109
|
-
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
110
|
-
<span className="truncate font-semibold">{user.name}</span>
|
|
111
|
-
<span className="truncate text-xs">{user.email}</span>
|
|
112
|
-
</div>
|
|
113
|
-
<button
|
|
114
|
-
onClick={async () => {
|
|
115
|
-
await authClient.signOut()
|
|
116
|
-
window.location.reload()
|
|
117
|
-
}}
|
|
118
|
-
className="ml-auto"
|
|
98
|
+
{user ? (
|
|
99
|
+
<SidebarMenuButton
|
|
100
|
+
size="lg"
|
|
101
|
+
render={<div />}
|
|
102
|
+
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
119
103
|
>
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
104
|
+
<div className="flex items-center gap-2 w-full">
|
|
105
|
+
<Avatar className="h-8 w-8 rounded-lg">
|
|
106
|
+
<AvatarImage src={user.image || ""} alt={user.name} />
|
|
107
|
+
<AvatarFallback className="rounded-lg">
|
|
108
|
+
{user.name.charAt(0) || "U"}
|
|
109
|
+
</AvatarFallback>
|
|
110
|
+
</Avatar>
|
|
111
|
+
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
112
|
+
<span className="truncate font-semibold">{user.name}</span>
|
|
113
|
+
<span className="truncate text-xs">{user.email}</span>
|
|
114
|
+
</div>
|
|
115
|
+
<div className="flex items-center gap-1 ml-auto">
|
|
116
|
+
<Link
|
|
117
|
+
to="/account"
|
|
118
|
+
className="p-1 rounded hover:bg-sidebar-accent-foreground/10"
|
|
119
|
+
title="Account Settings"
|
|
120
|
+
>
|
|
121
|
+
<Settings className="h-4 w-4" />
|
|
122
|
+
</Link>
|
|
123
|
+
<button
|
|
124
|
+
onClick={async (e) => {
|
|
125
|
+
e.preventDefault()
|
|
126
|
+
e.stopPropagation()
|
|
127
|
+
await authClient.signOut()
|
|
128
|
+
window.location.reload()
|
|
129
|
+
}}
|
|
130
|
+
className="p-1 rounded hover:bg-sidebar-accent-foreground/10"
|
|
131
|
+
title="Sign Out"
|
|
132
|
+
>
|
|
133
|
+
<LogOut className="h-4 w-4" />
|
|
134
|
+
</button>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</SidebarMenuButton>
|
|
138
|
+
) : (
|
|
124
139
|
<SidebarMenuButton
|
|
125
140
|
render={
|
|
126
141
|
<Link to="/login" className="flex items-center gap-2" />
|
|
@@ -19,7 +19,7 @@ import { useState } from "react"
|
|
|
19
19
|
import { Link } from "@tanstack/react-router"
|
|
20
20
|
import { useForm } from "@tanstack/react-form"
|
|
21
21
|
import { z } from "zod"
|
|
22
|
-
import { authClient } from "
|
|
22
|
+
import { authClient } from "@/lib/auth-client"
|
|
23
23
|
|
|
24
24
|
const loginSchema = z.object({
|
|
25
25
|
email: z.string().email("Invalid email address"),
|
|
@@ -18,7 +18,7 @@ import { useState } from "react"
|
|
|
18
18
|
import { Link } from "@tanstack/react-router"
|
|
19
19
|
import { useForm } from "@tanstack/react-form"
|
|
20
20
|
import { z } from "zod"
|
|
21
|
-
import { authClient } from "
|
|
21
|
+
import { authClient } from "@/lib/auth-client"
|
|
22
22
|
|
|
23
23
|
const signupSchema = z.object({
|
|
24
24
|
name: z.string().min(1, "Full name is required"),
|
|
@@ -17,6 +17,7 @@ import { Route as IsrRouteImport } from './routes/isr'
|
|
|
17
17
|
import { Route as DashboardRouteImport } from './routes/dashboard'
|
|
18
18
|
import { Route as ClientOrpcAuthRouteImport } from './routes/client-orpc-auth'
|
|
19
19
|
import { Route as ClientOrpcRouteImport } from './routes/client-orpc'
|
|
20
|
+
import { Route as AccountRouteImport } from './routes/account'
|
|
20
21
|
import { Route as IndexRouteImport } from './routes/index'
|
|
21
22
|
import { Route as ApiRpcSplatRouteImport } from './routes/api/rpc.$'
|
|
22
23
|
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
|
|
@@ -61,6 +62,11 @@ const ClientOrpcRoute = ClientOrpcRouteImport.update({
|
|
|
61
62
|
path: '/client-orpc',
|
|
62
63
|
getParentRoute: () => rootRouteImport,
|
|
63
64
|
} as any)
|
|
65
|
+
const AccountRoute = AccountRouteImport.update({
|
|
66
|
+
id: '/account',
|
|
67
|
+
path: '/account',
|
|
68
|
+
getParentRoute: () => rootRouteImport,
|
|
69
|
+
} as any)
|
|
64
70
|
const IndexRoute = IndexRouteImport.update({
|
|
65
71
|
id: '/',
|
|
66
72
|
path: '/',
|
|
@@ -79,6 +85,7 @@ const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
|
|
|
79
85
|
|
|
80
86
|
export interface FileRoutesByFullPath {
|
|
81
87
|
'/': typeof IndexRoute
|
|
88
|
+
'/account': typeof AccountRoute
|
|
82
89
|
'/client-orpc': typeof ClientOrpcRoute
|
|
83
90
|
'/client-orpc-auth': typeof ClientOrpcAuthRoute
|
|
84
91
|
'/dashboard': typeof DashboardRoute
|
|
@@ -92,6 +99,7 @@ export interface FileRoutesByFullPath {
|
|
|
92
99
|
}
|
|
93
100
|
export interface FileRoutesByTo {
|
|
94
101
|
'/': typeof IndexRoute
|
|
102
|
+
'/account': typeof AccountRoute
|
|
95
103
|
'/client-orpc': typeof ClientOrpcRoute
|
|
96
104
|
'/client-orpc-auth': typeof ClientOrpcAuthRoute
|
|
97
105
|
'/dashboard': typeof DashboardRoute
|
|
@@ -106,6 +114,7 @@ export interface FileRoutesByTo {
|
|
|
106
114
|
export interface FileRoutesById {
|
|
107
115
|
__root__: typeof rootRouteImport
|
|
108
116
|
'/': typeof IndexRoute
|
|
117
|
+
'/account': typeof AccountRoute
|
|
109
118
|
'/client-orpc': typeof ClientOrpcRoute
|
|
110
119
|
'/client-orpc-auth': typeof ClientOrpcAuthRoute
|
|
111
120
|
'/dashboard': typeof DashboardRoute
|
|
@@ -121,6 +130,7 @@ export interface FileRouteTypes {
|
|
|
121
130
|
fileRoutesByFullPath: FileRoutesByFullPath
|
|
122
131
|
fullPaths:
|
|
123
132
|
| '/'
|
|
133
|
+
| '/account'
|
|
124
134
|
| '/client-orpc'
|
|
125
135
|
| '/client-orpc-auth'
|
|
126
136
|
| '/dashboard'
|
|
@@ -134,6 +144,7 @@ export interface FileRouteTypes {
|
|
|
134
144
|
fileRoutesByTo: FileRoutesByTo
|
|
135
145
|
to:
|
|
136
146
|
| '/'
|
|
147
|
+
| '/account'
|
|
137
148
|
| '/client-orpc'
|
|
138
149
|
| '/client-orpc-auth'
|
|
139
150
|
| '/dashboard'
|
|
@@ -147,6 +158,7 @@ export interface FileRouteTypes {
|
|
|
147
158
|
id:
|
|
148
159
|
| '__root__'
|
|
149
160
|
| '/'
|
|
161
|
+
| '/account'
|
|
150
162
|
| '/client-orpc'
|
|
151
163
|
| '/client-orpc-auth'
|
|
152
164
|
| '/dashboard'
|
|
@@ -161,6 +173,7 @@ export interface FileRouteTypes {
|
|
|
161
173
|
}
|
|
162
174
|
export interface RootRouteChildren {
|
|
163
175
|
IndexRoute: typeof IndexRoute
|
|
176
|
+
AccountRoute: typeof AccountRoute
|
|
164
177
|
ClientOrpcRoute: typeof ClientOrpcRoute
|
|
165
178
|
ClientOrpcAuthRoute: typeof ClientOrpcAuthRoute
|
|
166
179
|
DashboardRoute: typeof DashboardRoute
|
|
@@ -231,6 +244,13 @@ declare module '@tanstack/react-router' {
|
|
|
231
244
|
preLoaderRoute: typeof ClientOrpcRouteImport
|
|
232
245
|
parentRoute: typeof rootRouteImport
|
|
233
246
|
}
|
|
247
|
+
'/account': {
|
|
248
|
+
id: '/account'
|
|
249
|
+
path: '/account'
|
|
250
|
+
fullPath: '/account'
|
|
251
|
+
preLoaderRoute: typeof AccountRouteImport
|
|
252
|
+
parentRoute: typeof rootRouteImport
|
|
253
|
+
}
|
|
234
254
|
'/': {
|
|
235
255
|
id: '/'
|
|
236
256
|
path: '/'
|
|
@@ -257,6 +277,7 @@ declare module '@tanstack/react-router' {
|
|
|
257
277
|
|
|
258
278
|
const rootRouteChildren: RootRouteChildren = {
|
|
259
279
|
IndexRoute: IndexRoute,
|
|
280
|
+
AccountRoute: AccountRoute,
|
|
260
281
|
ClientOrpcRoute: ClientOrpcRoute,
|
|
261
282
|
ClientOrpcAuthRoute: ClientOrpcAuthRoute,
|
|
262
283
|
DashboardRoute: DashboardRoute,
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { createFileRoute, redirect } from "@tanstack/react-router"
|
|
3
|
+
import { useForm } from "@tanstack/react-form"
|
|
4
|
+
import { z } from "zod"
|
|
5
|
+
import { toast } from "sonner"
|
|
6
|
+
import { Loader2, User } from "lucide-react"
|
|
7
|
+
|
|
8
|
+
import { Button } from "@workspace/ui/components/button"
|
|
9
|
+
import { Input } from "@workspace/ui/components/input"
|
|
10
|
+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@workspace/ui/components/card"
|
|
11
|
+
import { Field, FieldError, FieldLabel } from "@workspace/ui/components/field"
|
|
12
|
+
import { Avatar, AvatarFallback, AvatarImage } from "@workspace/ui/components/avatar"
|
|
13
|
+
import { Separator } from "@workspace/ui/components/separator"
|
|
14
|
+
|
|
15
|
+
import { authClient } from "@/lib/auth-client"
|
|
16
|
+
import { getSessionFn } from "@/lib/auth-utils"
|
|
17
|
+
|
|
18
|
+
const profileSchema = z.object({
|
|
19
|
+
name: z.string().min(1, "Name is required"),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const passwordSchema = z.object({
|
|
23
|
+
currentPassword: z.string().min(1, "Current password is required"),
|
|
24
|
+
newPassword: z.string().min(8, "New password must be at least 8 characters long"),
|
|
25
|
+
confirmPassword: z.string().min(1, "Please confirm your new password"),
|
|
26
|
+
}).refine((data) => data.newPassword === data.confirmPassword, {
|
|
27
|
+
message: "Passwords do not match",
|
|
28
|
+
path: ["confirmPassword"],
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
export const Route = createFileRoute("/account")({
|
|
32
|
+
beforeLoad: async () => {
|
|
33
|
+
const session = await getSessionFn()
|
|
34
|
+
if (!session) {
|
|
35
|
+
throw redirect({
|
|
36
|
+
to: "/login",
|
|
37
|
+
search: {
|
|
38
|
+
redirect: "/account",
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
return { session }
|
|
43
|
+
},
|
|
44
|
+
component: AccountPage,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
function AccountPage() {
|
|
48
|
+
const { session } = Route.useRouteContext()
|
|
49
|
+
const [loading, setLoading] = React.useState(false)
|
|
50
|
+
const user = session.user
|
|
51
|
+
|
|
52
|
+
const profileForm = useForm({
|
|
53
|
+
defaultValues: {
|
|
54
|
+
name: user.name || "",
|
|
55
|
+
},
|
|
56
|
+
validators: {
|
|
57
|
+
onChange: profileSchema,
|
|
58
|
+
},
|
|
59
|
+
onSubmit: async ({ value }) => {
|
|
60
|
+
setLoading(true)
|
|
61
|
+
const { error } = await authClient.updateUser({
|
|
62
|
+
name: value.name,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
if (error) {
|
|
66
|
+
toast.error(error.message || "Failed to update profile")
|
|
67
|
+
} else {
|
|
68
|
+
toast.success("Profile updated successfully")
|
|
69
|
+
}
|
|
70
|
+
setLoading(false)
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const passwordForm = useForm({
|
|
75
|
+
defaultValues: {
|
|
76
|
+
currentPassword: "",
|
|
77
|
+
newPassword: "",
|
|
78
|
+
confirmPassword: "",
|
|
79
|
+
},
|
|
80
|
+
validators: {
|
|
81
|
+
onChange: passwordSchema,
|
|
82
|
+
},
|
|
83
|
+
onSubmit: async ({ value }) => {
|
|
84
|
+
setLoading(true)
|
|
85
|
+
const { error } = await authClient.changePassword({
|
|
86
|
+
currentPassword: value.currentPassword,
|
|
87
|
+
newPassword: value.newPassword,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
if (error) {
|
|
91
|
+
toast.error(error.message || "Failed to change password")
|
|
92
|
+
} else {
|
|
93
|
+
toast.success("Password changed successfully")
|
|
94
|
+
passwordForm.reset()
|
|
95
|
+
}
|
|
96
|
+
setLoading(false)
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div className="container max-w-4xl py-10">
|
|
102
|
+
<div className="flex flex-col gap-8">
|
|
103
|
+
<div>
|
|
104
|
+
<h1 className="text-3xl font-bold">Account Settings</h1>
|
|
105
|
+
<p className="text-muted-foreground">Manage your profile and account preferences.</p>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<Separator />
|
|
109
|
+
|
|
110
|
+
<div className="grid gap-8">
|
|
111
|
+
{/* Profile Section */}
|
|
112
|
+
<Card>
|
|
113
|
+
<CardHeader>
|
|
114
|
+
<CardTitle>Profile</CardTitle>
|
|
115
|
+
<CardDescription>Update your personal information.</CardDescription>
|
|
116
|
+
</CardHeader>
|
|
117
|
+
<CardContent className="space-y-6">
|
|
118
|
+
<div className="flex items-center gap-4">
|
|
119
|
+
<Avatar className="h-20 w-20">
|
|
120
|
+
<AvatarImage src={user.image || ""} />
|
|
121
|
+
<AvatarFallback className="text-2xl">
|
|
122
|
+
{user.name.charAt(0) || <User className="h-10 w-10" />}
|
|
123
|
+
</AvatarFallback>
|
|
124
|
+
</Avatar>
|
|
125
|
+
<div className="space-y-1">
|
|
126
|
+
<p className="text-sm font-medium leading-none">{user.name}</p>
|
|
127
|
+
<p className="text-sm text-muted-foreground">{user.email}</p>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<form
|
|
132
|
+
onSubmit={(e) => {
|
|
133
|
+
e.preventDefault()
|
|
134
|
+
e.stopPropagation()
|
|
135
|
+
profileForm.handleSubmit()
|
|
136
|
+
}}
|
|
137
|
+
className="space-y-4"
|
|
138
|
+
>
|
|
139
|
+
<profileForm.Field
|
|
140
|
+
name="name"
|
|
141
|
+
children={(field) => (
|
|
142
|
+
<Field>
|
|
143
|
+
<FieldLabel htmlFor={field.name}>Display Name</FieldLabel>
|
|
144
|
+
<Input
|
|
145
|
+
id={field.name}
|
|
146
|
+
value={field.state.value}
|
|
147
|
+
onBlur={field.handleBlur}
|
|
148
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
149
|
+
/>
|
|
150
|
+
<FieldError />
|
|
151
|
+
</Field>
|
|
152
|
+
)}
|
|
153
|
+
/>
|
|
154
|
+
<Button type="submit" disabled={loading}>
|
|
155
|
+
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
156
|
+
Update Profile
|
|
157
|
+
</Button>
|
|
158
|
+
</form>
|
|
159
|
+
</CardContent>
|
|
160
|
+
</Card>
|
|
161
|
+
|
|
162
|
+
{/* Password Section */}
|
|
163
|
+
<Card>
|
|
164
|
+
<CardHeader>
|
|
165
|
+
<CardTitle>Security</CardTitle>
|
|
166
|
+
<CardDescription>Change your password to keep your account secure.</CardDescription>
|
|
167
|
+
</CardHeader>
|
|
168
|
+
<CardContent>
|
|
169
|
+
<form
|
|
170
|
+
onSubmit={(e) => {
|
|
171
|
+
e.preventDefault()
|
|
172
|
+
e.stopPropagation()
|
|
173
|
+
passwordForm.handleSubmit()
|
|
174
|
+
}}
|
|
175
|
+
className="space-y-4"
|
|
176
|
+
>
|
|
177
|
+
<passwordForm.Field
|
|
178
|
+
name="currentPassword"
|
|
179
|
+
children={(field) => (
|
|
180
|
+
<Field>
|
|
181
|
+
<FieldLabel htmlFor={field.name}>Current Password</FieldLabel>
|
|
182
|
+
<Input
|
|
183
|
+
id={field.name}
|
|
184
|
+
type="password"
|
|
185
|
+
value={field.state.value}
|
|
186
|
+
onBlur={field.handleBlur}
|
|
187
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
188
|
+
/>
|
|
189
|
+
<FieldError />
|
|
190
|
+
</Field>
|
|
191
|
+
)}
|
|
192
|
+
/>
|
|
193
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
194
|
+
<passwordForm.Field
|
|
195
|
+
name="newPassword"
|
|
196
|
+
children={(field) => (
|
|
197
|
+
<Field>
|
|
198
|
+
<FieldLabel htmlFor={field.name}>New Password</FieldLabel>
|
|
199
|
+
<Input
|
|
200
|
+
id={field.name}
|
|
201
|
+
type="password"
|
|
202
|
+
value={field.state.value}
|
|
203
|
+
onBlur={field.handleBlur}
|
|
204
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
205
|
+
/>
|
|
206
|
+
<FieldError />
|
|
207
|
+
</Field>
|
|
208
|
+
)}
|
|
209
|
+
/>
|
|
210
|
+
<passwordForm.Field
|
|
211
|
+
name="confirmPassword"
|
|
212
|
+
children={(field) => (
|
|
213
|
+
<Field>
|
|
214
|
+
<FieldLabel htmlFor={field.name}>Confirm New Password</FieldLabel>
|
|
215
|
+
<Input
|
|
216
|
+
id={field.name}
|
|
217
|
+
type="password"
|
|
218
|
+
value={field.state.value}
|
|
219
|
+
onBlur={field.handleBlur}
|
|
220
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
221
|
+
/>
|
|
222
|
+
<FieldError />
|
|
223
|
+
</Field>
|
|
224
|
+
)}
|
|
225
|
+
/>
|
|
226
|
+
</div>
|
|
227
|
+
<Button type="submit" variant="secondary" disabled={loading}>
|
|
228
|
+
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
229
|
+
Change Password
|
|
230
|
+
</Button>
|
|
231
|
+
</form>
|
|
232
|
+
</CardContent>
|
|
233
|
+
</Card>
|
|
234
|
+
|
|
235
|
+
{/* Danger Zone */}
|
|
236
|
+
<Card className="border-destructive/50">
|
|
237
|
+
<CardHeader>
|
|
238
|
+
<CardTitle className="text-destructive">Danger Zone</CardTitle>
|
|
239
|
+
<CardDescription>Permanently delete your account and all data.</CardDescription>
|
|
240
|
+
</CardHeader>
|
|
241
|
+
<CardFooter>
|
|
242
|
+
<Button
|
|
243
|
+
variant="destructive"
|
|
244
|
+
onClick={async () => {
|
|
245
|
+
if (confirm("Are you sure you want to delete your account? This action cannot be undone.")) {
|
|
246
|
+
const { error } = await authClient.deleteUser()
|
|
247
|
+
if (error) {
|
|
248
|
+
toast.error(error.message || "Failed to delete account")
|
|
249
|
+
} else {
|
|
250
|
+
window.location.href = "/"
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}}
|
|
254
|
+
>
|
|
255
|
+
Delete Account
|
|
256
|
+
</Button>
|
|
257
|
+
</CardFooter>
|
|
258
|
+
</Card>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createFileRoute, redirect } from "@tanstack/react-router"
|
|
2
2
|
import { useQuery } from "@tanstack/react-query"
|
|
3
|
-
import { getSessionFn } from "
|
|
4
|
-
import { orpc } from "
|
|
3
|
+
import { getSessionFn } from "@/lib/auth-utils"
|
|
4
|
+
import { orpc } from "@/lib/orpc"
|
|
5
5
|
|
|
6
6
|
export const Route = createFileRoute("/client-orpc-auth")({
|
|
7
7
|
beforeLoad: async () => {
|
|
@@ -14,7 +14,13 @@ import {
|
|
|
14
14
|
FieldLabel,
|
|
15
15
|
} from "@workspace/ui/components/field"
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import type { router } from "@workspace/orpc/router"
|
|
18
|
+
import type { InferRouterInputs, InferRouterOutputs } from "@orpc/server"
|
|
19
|
+
import { orpc } from "@/lib/orpc"
|
|
20
|
+
|
|
21
|
+
type Inputs = InferRouterInputs<typeof router>
|
|
22
|
+
type Outputs = InferRouterOutputs<typeof router>
|
|
23
|
+
type Planet = Outputs["planets"]["getPlanets"][number]
|
|
18
24
|
|
|
19
25
|
const planetSchema = z.object({
|
|
20
26
|
name: z.string().min(1, "Name is required"),
|
|
@@ -33,7 +39,7 @@ function ClientORPC() {
|
|
|
33
39
|
|
|
34
40
|
const { data: planets = [], isLoading } = useQuery({
|
|
35
41
|
queryKey: ["planets"],
|
|
36
|
-
queryFn: () => orpc.getPlanets(),
|
|
42
|
+
queryFn: () => orpc.planets.getPlanets(),
|
|
37
43
|
})
|
|
38
44
|
|
|
39
45
|
const form = useForm({
|
|
@@ -74,46 +80,46 @@ function ClientORPC() {
|
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
const createMutation = useMutation({
|
|
77
|
-
mutationFn: (
|
|
83
|
+
mutationFn: (input: Inputs["planets"]["createPlanet"]) => orpc.planets.createPlanet(input),
|
|
78
84
|
onSuccess: () => {
|
|
79
85
|
queryClient.invalidateQueries({ queryKey: ["planets"] })
|
|
80
86
|
resetForm()
|
|
81
87
|
toast.success("Planet added successfully")
|
|
82
88
|
},
|
|
83
|
-
onError: (err
|
|
89
|
+
onError: (err) => {
|
|
84
90
|
toast.error(err.message || "Failed to add planet")
|
|
85
91
|
},
|
|
86
92
|
})
|
|
87
93
|
|
|
88
94
|
const updateMutation = useMutation({
|
|
89
|
-
mutationFn: (
|
|
95
|
+
mutationFn: (input: Inputs["planets"]["updatePlanet"]) => orpc.planets.updatePlanet(input),
|
|
90
96
|
onSuccess: () => {
|
|
91
97
|
queryClient.invalidateQueries({ queryKey: ["planets"] })
|
|
92
98
|
resetForm()
|
|
93
99
|
toast.success("Planet updated successfully")
|
|
94
100
|
},
|
|
95
|
-
onError: (err
|
|
101
|
+
onError: (err) => {
|
|
96
102
|
toast.error(err.message || "Failed to update planet")
|
|
97
103
|
},
|
|
98
104
|
})
|
|
99
105
|
|
|
100
106
|
const deleteMutation = useMutation({
|
|
101
|
-
mutationFn: (
|
|
107
|
+
mutationFn: (input: Inputs["planets"]["deletePlanet"]) => orpc.planets.deletePlanet(input),
|
|
102
108
|
onSuccess: () => {
|
|
103
109
|
queryClient.invalidateQueries({ queryKey: ["planets"] })
|
|
104
110
|
toast.success("Planet deleted successfully")
|
|
105
111
|
},
|
|
106
|
-
onError: (err
|
|
112
|
+
onError: (err) => {
|
|
107
113
|
toast.error(err.message || "Failed to delete planet")
|
|
108
114
|
},
|
|
109
115
|
})
|
|
110
116
|
|
|
111
117
|
const handleDelete = async (id: number) => {
|
|
112
118
|
if (!confirm("Are you sure you want to delete this planet?")) return
|
|
113
|
-
await deleteMutation.mutateAsync(id)
|
|
119
|
+
await deleteMutation.mutateAsync({ id })
|
|
114
120
|
}
|
|
115
121
|
|
|
116
|
-
const startEdit = (planet:
|
|
122
|
+
const startEdit = (planet: Planet) => {
|
|
117
123
|
setEditingId(planet.id)
|
|
118
124
|
form.setFieldValue("name", planet.name)
|
|
119
125
|
form.setFieldValue("description", planet.description || "")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createFileRoute, redirect } from "@tanstack/react-router"
|
|
2
|
-
import { getSessionFn } from "
|
|
3
|
-
import { authClient } from "
|
|
4
|
-
import { orpc } from "
|
|
2
|
+
import { getSessionFn } from "@/lib/auth-utils"
|
|
3
|
+
import { authClient } from "@/lib/auth-client"
|
|
4
|
+
import { orpc } from "@/lib/orpc"
|
|
5
5
|
|
|
6
6
|
export const Route = createFileRoute("/dashboard")({
|
|
7
7
|
beforeLoad: async () => {
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { Link, createFileRoute } from "@tanstack/react-router"
|
|
2
2
|
import { Button } from "@workspace/ui/components/button"
|
|
3
|
-
import {
|
|
3
|
+
import type { router } from "@workspace/orpc/router"
|
|
4
|
+
import type { InferRouterOutputs } from "@orpc/server"
|
|
5
|
+
import { orpc } from "@/lib/orpc"
|
|
6
|
+
|
|
7
|
+
type Outputs = InferRouterOutputs<typeof router>
|
|
8
|
+
type Planet = Outputs["planets"]["getPlanets"][number]
|
|
4
9
|
|
|
5
10
|
export const Route = createFileRoute("/")({
|
|
6
11
|
loader: async () => {
|
|
7
12
|
const [helloRes, planets] = await Promise.all([
|
|
8
13
|
orpc.hello({ name: "TanStack Start" }),
|
|
9
|
-
orpc.getPlanets(),
|
|
14
|
+
orpc.planets.getPlanets(),
|
|
10
15
|
])
|
|
11
16
|
return {
|
|
12
17
|
message: helloRes.message,
|
|
@@ -32,7 +37,7 @@ function App() {
|
|
|
32
37
|
<p className="text-gray-500 italic">No planets found in the database. Run `db:push` and seed data if needed.</p>
|
|
33
38
|
) : (
|
|
34
39
|
<ul className="grid grid-cols-1 gap-2">
|
|
35
|
-
{planets.map((planet) => (
|
|
40
|
+
{planets.map((planet: Planet) => (
|
|
36
41
|
<li key={planet.id} className="rounded-md border p-3 bg-white shadow-sm">
|
|
37
42
|
<span className="font-bold">{planet.name}</span> - {planet.description}
|
|
38
43
|
</li>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router"
|
|
2
|
-
import { orpc } from "
|
|
2
|
+
import { orpc } from "@/lib/orpc"
|
|
3
3
|
|
|
4
4
|
export const Route = createFileRoute("/isr")({
|
|
5
5
|
loader: async () => {
|
|
6
6
|
// In a real ISR scenario, this would be cached on the server
|
|
7
7
|
// For this example, we'll fetch planets via oRPC
|
|
8
|
-
const planets = await orpc.getPlanets()
|
|
8
|
+
const planets = await orpc.planets.getPlanets()
|
|
9
9
|
|
|
10
10
|
return {
|
|
11
11
|
generatedAt: new Date().toISOString(),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createFileRoute, redirect } from "@tanstack/react-router"
|
|
2
|
-
import { getSessionFn } from "
|
|
3
|
-
import { orpc } from "
|
|
2
|
+
import { getSessionFn } from "@/lib/auth-utils"
|
|
3
|
+
import { orpc } from "@/lib/orpc"
|
|
4
4
|
|
|
5
5
|
export const Route = createFileRoute("/ssr-orpc-auth")({
|
|
6
6
|
beforeLoad: async () => {
|
|
@@ -13,7 +13,12 @@ import {
|
|
|
13
13
|
FieldLabel,
|
|
14
14
|
} from "@workspace/ui/components/field"
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import type { router } from "@workspace/orpc/router"
|
|
17
|
+
import type { InferRouterOutputs } from "@orpc/server"
|
|
18
|
+
import { orpc } from "@/lib/orpc"
|
|
19
|
+
|
|
20
|
+
type Outputs = InferRouterOutputs<typeof router>
|
|
21
|
+
type Planet = Outputs["planets"]["getPlanets"][number]
|
|
17
22
|
|
|
18
23
|
const planetSchema = z.object({
|
|
19
24
|
name: z.string().min(1, "Name is required"),
|
|
@@ -24,7 +29,7 @@ const planetSchema = z.object({
|
|
|
24
29
|
|
|
25
30
|
export const Route = createFileRoute("/ssr-orpc")({
|
|
26
31
|
loader: async () => {
|
|
27
|
-
const planets = await orpc.getPlanets()
|
|
32
|
+
const planets = await orpc.planets.getPlanets()
|
|
28
33
|
return { planets }
|
|
29
34
|
},
|
|
30
35
|
component: SSRORPC,
|
|
@@ -49,7 +54,7 @@ function SSRORPC() {
|
|
|
49
54
|
const toastId = toast.loading(editingId ? "Updating planet..." : "Adding planet...")
|
|
50
55
|
try {
|
|
51
56
|
if (editingId) {
|
|
52
|
-
await orpc.updatePlanet({
|
|
57
|
+
await orpc.planets.updatePlanet({
|
|
53
58
|
id: editingId,
|
|
54
59
|
name: value.name,
|
|
55
60
|
description: value.description,
|
|
@@ -59,7 +64,7 @@ function SSRORPC() {
|
|
|
59
64
|
})
|
|
60
65
|
toast.success("Planet updated successfully", { id: toastId })
|
|
61
66
|
} else {
|
|
62
|
-
await orpc.createPlanet({
|
|
67
|
+
await orpc.planets.createPlanet({
|
|
63
68
|
name: value.name,
|
|
64
69
|
description: value.description,
|
|
65
70
|
distanceFromSun: parseFloat(value.distance),
|
|
@@ -86,7 +91,7 @@ function SSRORPC() {
|
|
|
86
91
|
if (!confirm("Are you sure you want to delete this planet?")) return
|
|
87
92
|
const toastId = toast.loading("Deleting planet...")
|
|
88
93
|
try {
|
|
89
|
-
await orpc.deletePlanet({ id })
|
|
94
|
+
await orpc.planets.deletePlanet({ id })
|
|
90
95
|
await router.invalidate()
|
|
91
96
|
toast.success("Planet deleted successfully", { id: toastId })
|
|
92
97
|
} catch (err: any) {
|
|
@@ -95,7 +100,7 @@ function SSRORPC() {
|
|
|
95
100
|
}
|
|
96
101
|
}
|
|
97
102
|
|
|
98
|
-
const startEdit = (planet:
|
|
103
|
+
const startEdit = (planet: Planet) => {
|
|
99
104
|
setEditingId(planet.id)
|
|
100
105
|
form.setFieldValue("name", planet.name)
|
|
101
106
|
form.setFieldValue("description", planet.description || "")
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ORPCError, os } from '@orpc/server'
|
|
2
|
+
import { db, schema } from '@workspace/db'
|
|
3
|
+
import { eq } from 'drizzle-orm'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import type { RPCContext } from './router'
|
|
6
|
+
|
|
7
|
+
const { planets } = schema
|
|
8
|
+
|
|
9
|
+
const o = os.$context<RPCContext>()
|
|
10
|
+
|
|
11
|
+
export const planetRouter = o.router({
|
|
12
|
+
getPlanets: o
|
|
13
|
+
.handler(async () => {
|
|
14
|
+
const allPlanets = await db.select().from(planets)
|
|
15
|
+
return allPlanets
|
|
16
|
+
}),
|
|
17
|
+
|
|
18
|
+
createPlanet: o
|
|
19
|
+
.input(
|
|
20
|
+
z.object({
|
|
21
|
+
name: z.string().min(1),
|
|
22
|
+
description: z.string().optional(),
|
|
23
|
+
distanceFromSun: z.number(),
|
|
24
|
+
diameter: z.number(),
|
|
25
|
+
hasRings: z.boolean().default(false),
|
|
26
|
+
atmosphere: z.string().optional(),
|
|
27
|
+
}),
|
|
28
|
+
)
|
|
29
|
+
.handler(async ({ input }) => {
|
|
30
|
+
const [newPlanet] = await db.insert(planets).values(input).returning()
|
|
31
|
+
return newPlanet
|
|
32
|
+
}),
|
|
33
|
+
|
|
34
|
+
updatePlanet: o
|
|
35
|
+
.input(
|
|
36
|
+
z.object({
|
|
37
|
+
id: z.number(),
|
|
38
|
+
name: z.string().min(1),
|
|
39
|
+
description: z.string().optional(),
|
|
40
|
+
distanceFromSun: z.number(),
|
|
41
|
+
diameter: z.number(),
|
|
42
|
+
hasRings: z.boolean(),
|
|
43
|
+
atmosphere: z.string().optional(),
|
|
44
|
+
}),
|
|
45
|
+
)
|
|
46
|
+
.handler(async ({ input }) => {
|
|
47
|
+
const { id, ...data } = input
|
|
48
|
+
const results = await db
|
|
49
|
+
.update(planets)
|
|
50
|
+
.set({ ...data, updatedAt: new Date() })
|
|
51
|
+
.where(eq(planets.id, id))
|
|
52
|
+
.returning()
|
|
53
|
+
|
|
54
|
+
if (results.length === 0) {
|
|
55
|
+
throw new ORPCError('NOT_FOUND')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return results[0]
|
|
59
|
+
}),
|
|
60
|
+
|
|
61
|
+
deletePlanet: o
|
|
62
|
+
.input(
|
|
63
|
+
z.object({
|
|
64
|
+
id: z.number(),
|
|
65
|
+
}),
|
|
66
|
+
)
|
|
67
|
+
.handler(async ({ input }) => {
|
|
68
|
+
const results = await db
|
|
69
|
+
.delete(planets)
|
|
70
|
+
.where(eq(planets.id, input.id))
|
|
71
|
+
.returning()
|
|
72
|
+
|
|
73
|
+
if (results.length === 0) {
|
|
74
|
+
throw new ORPCError('NOT_FOUND')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return results[0]
|
|
78
|
+
}),
|
|
79
|
+
})
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { ORPCError, os } from '@orpc/server'
|
|
2
|
-
import { db, schema } from '@workspace/db'
|
|
3
|
-
import { eq } from 'drizzle-orm'
|
|
4
2
|
import { z } from 'zod'
|
|
3
|
+
import { planetRouter } from './planets'
|
|
5
4
|
import type { Session } from '@workspace/auth/lib/auth'
|
|
6
5
|
|
|
7
6
|
export type RPCContext = {
|
|
8
7
|
session: Session | null
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
const { planets } = schema
|
|
12
|
-
|
|
13
10
|
const o = os.$context<RPCContext>()
|
|
14
11
|
|
|
15
12
|
export const router = o.router({
|
|
13
|
+
planets: planetRouter,
|
|
14
|
+
|
|
16
15
|
hello: o
|
|
17
16
|
.input(
|
|
18
17
|
z.object({
|
|
@@ -25,12 +24,6 @@ export const router = o.router({
|
|
|
25
24
|
}
|
|
26
25
|
}),
|
|
27
26
|
|
|
28
|
-
getPlanets: o
|
|
29
|
-
.handler(async () => {
|
|
30
|
-
const allPlanets = await db.select().from(planets)
|
|
31
|
-
return allPlanets
|
|
32
|
-
}),
|
|
33
|
-
|
|
34
27
|
getSecretData: o
|
|
35
28
|
.use(async ({ context, next }) => {
|
|
36
29
|
if (!context.session) {
|
|
@@ -48,68 +41,6 @@ export const router = o.router({
|
|
|
48
41
|
email: context.session.user.email,
|
|
49
42
|
}
|
|
50
43
|
}),
|
|
51
|
-
|
|
52
|
-
createPlanet: o
|
|
53
|
-
.input(
|
|
54
|
-
z.object({
|
|
55
|
-
name: z.string().min(1),
|
|
56
|
-
description: z.string().optional(),
|
|
57
|
-
distanceFromSun: z.number(),
|
|
58
|
-
diameter: z.number(),
|
|
59
|
-
hasRings: z.boolean().default(false),
|
|
60
|
-
atmosphere: z.string().optional(),
|
|
61
|
-
}),
|
|
62
|
-
)
|
|
63
|
-
.handler(async ({ input }) => {
|
|
64
|
-
const [newPlanet] = await db.insert(planets).values(input).returning()
|
|
65
|
-
return newPlanet
|
|
66
|
-
}),
|
|
67
|
-
|
|
68
|
-
updatePlanet: o
|
|
69
|
-
.input(
|
|
70
|
-
z.object({
|
|
71
|
-
id: z.number(),
|
|
72
|
-
name: z.string().min(1),
|
|
73
|
-
description: z.string().optional(),
|
|
74
|
-
distanceFromSun: z.number(),
|
|
75
|
-
diameter: z.number(),
|
|
76
|
-
hasRings: z.boolean(),
|
|
77
|
-
atmosphere: z.string().optional(),
|
|
78
|
-
}),
|
|
79
|
-
)
|
|
80
|
-
.handler(async ({ input }) => {
|
|
81
|
-
const { id, ...data } = input
|
|
82
|
-
const results = await db
|
|
83
|
-
.update(planets)
|
|
84
|
-
.set({ ...data, updatedAt: new Date() })
|
|
85
|
-
.where(eq(planets.id, id))
|
|
86
|
-
.returning()
|
|
87
|
-
|
|
88
|
-
if (results.length === 0) {
|
|
89
|
-
throw new ORPCError('NOT_FOUND')
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return results[0]
|
|
93
|
-
}),
|
|
94
|
-
|
|
95
|
-
deletePlanet: o
|
|
96
|
-
.input(
|
|
97
|
-
z.object({
|
|
98
|
-
id: z.number(),
|
|
99
|
-
}),
|
|
100
|
-
)
|
|
101
|
-
.handler(async ({ input }) => {
|
|
102
|
-
const results = await db
|
|
103
|
-
.delete(planets)
|
|
104
|
-
.where(eq(planets.id, input.id))
|
|
105
|
-
.returning()
|
|
106
|
-
|
|
107
|
-
if (results.length === 0) {
|
|
108
|
-
throw new ORPCError('NOT_FOUND')
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return results[0]
|
|
112
|
-
}),
|
|
113
44
|
})
|
|
114
45
|
|
|
115
46
|
export type AppRouter = typeof router
|