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 CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.9",
6
+ "version": "0.1.10",
7
7
  "description": "Scaffold a new project using the Croissant Stack",
8
8
  "repository": {
9
9
  "type": "git",
@@ -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 "../lib/auth-client"
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<any>(null)
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
- {user ? (
99
- <SidebarMenuButton
100
- size="lg"
101
- className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
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
- <LogOut className="h-4 w-4" />
121
- </button>
122
- </SidebarMenuButton>
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 "../lib/auth-client"
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 "../lib/auth-client"
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 "../lib/auth-utils"
4
- import { orpc } from "../lib/orpc"
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 { orpc } from "../lib/orpc"
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: (newPlanet: any) => orpc.createPlanet(newPlanet),
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: any) => {
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: (updatedPlanet: any) => orpc.updatePlanet(updatedPlanet),
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: any) => {
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: (id: number) => orpc.deletePlanet({ id }),
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: any) => {
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: any) => {
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 "../lib/auth-utils"
3
- import { authClient } from "../lib/auth-client"
4
- import { orpc } from "../lib/orpc"
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 { orpc } from "../lib/orpc"
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 "../lib/orpc"
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,5 +1,5 @@
1
1
  import { createFileRoute } from "@tanstack/react-router"
2
- import { LoginForm } from "../components/login-form"
2
+ import { LoginForm } from "@/components/login-form"
3
3
 
4
4
  export const Route = createFileRoute("/login")({
5
5
  component: Login,
@@ -1,5 +1,5 @@
1
1
  import { createFileRoute } from "@tanstack/react-router"
2
- import { SignupForm } from "../components/signup-form"
2
+ import { SignupForm } from "@/components/signup-form"
3
3
 
4
4
  export const Route = createFileRoute("/signup")({
5
5
  component: Signup,
@@ -1,6 +1,6 @@
1
1
  import { createFileRoute, redirect } from "@tanstack/react-router"
2
- import { getSessionFn } from "../lib/auth-utils"
3
- import { orpc } from "../lib/orpc"
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 { orpc } from "../lib/orpc"
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: any) => {
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