create-croissant 0.1.7 → 0.1.8
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/package.json +2 -0
- package/template/apps/web/src/routes/__root.tsx +2 -0
- package/template/apps/web/src/routes/client-orpc.tsx +170 -20
- package/template/apps/web/src/routes/isr.tsx +4 -0
- package/template/apps/web/src/routes/ssr-orpc.tsx +172 -18
- package/template/apps/web/vite.config.ts +5 -1
- package/template/packages/orpc/src/lib/router.ts +63 -0
package/package.json
CHANGED
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
"@noble/ciphers": "^2.2.0",
|
|
19
19
|
"@orpc/client": "^1.14.0",
|
|
20
20
|
"@orpc/server": "^1.14.0",
|
|
21
|
+
"@orpc/tanstack-query": "^1.14.0",
|
|
21
22
|
"@tailwindcss/vite": "^4.2.4",
|
|
23
|
+
"@tanstack/react-query": "^5.99.2",
|
|
22
24
|
"@tanstack/react-router": "^1.132.0",
|
|
23
25
|
"@tanstack/react-start": "^1.132.0",
|
|
24
26
|
"@tanstack/router-plugin": "^1.132.0",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router"
|
|
2
2
|
import { SidebarProvider, SidebarTrigger } from "@workspace/ui/components/sidebar"
|
|
3
|
+
import { Toaster } from "@workspace/ui/components/sonner"
|
|
3
4
|
|
|
4
5
|
import appCss from "@workspace/ui/globals.css?url"
|
|
5
6
|
import { AppSidebar } from "@/components/app-sidebar"
|
|
@@ -45,6 +46,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
|
|
45
46
|
{children}
|
|
46
47
|
</div>
|
|
47
48
|
</main>
|
|
49
|
+
<Toaster />
|
|
48
50
|
</SidebarProvider>
|
|
49
51
|
<Scripts />
|
|
50
52
|
</body>
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
2
|
import { createFileRoute } from "@tanstack/react-router"
|
|
3
|
+
import { Check, Pencil, Plus, Trash2 } from "lucide-react"
|
|
4
|
+
import { toast } from "sonner"
|
|
5
|
+
|
|
6
|
+
import { Button } from "@workspace/ui/components/button"
|
|
7
|
+
import { Input } from "@workspace/ui/components/input"
|
|
8
|
+
|
|
3
9
|
import { orpc } from "../lib/orpc"
|
|
4
10
|
|
|
5
11
|
export const Route = createFileRoute("/client-orpc")({
|
|
@@ -7,35 +13,179 @@ export const Route = createFileRoute("/client-orpc")({
|
|
|
7
13
|
})
|
|
8
14
|
|
|
9
15
|
function ClientORPC() {
|
|
10
|
-
const [
|
|
16
|
+
const [planets, setPlanets] = React.useState<Array<any>>([])
|
|
11
17
|
const [loading, setLoading] = React.useState(true)
|
|
18
|
+
const [editingId, setEditingId] = React.useState<number | null>(null)
|
|
19
|
+
|
|
20
|
+
// Form states
|
|
21
|
+
const [name, setName] = React.useState("")
|
|
22
|
+
const [description, setDescription] = React.useState("")
|
|
23
|
+
const [distance, setDistance] = React.useState("0")
|
|
24
|
+
const [diameter, setDiameter] = React.useState("0")
|
|
12
25
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
setLoading(false)
|
|
22
|
-
}
|
|
26
|
+
const fetchPlanets = async () => {
|
|
27
|
+
try {
|
|
28
|
+
const res = await orpc.getPlanets()
|
|
29
|
+
setPlanets(res)
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error(err)
|
|
32
|
+
} finally {
|
|
33
|
+
setLoading(false)
|
|
23
34
|
}
|
|
35
|
+
}
|
|
24
36
|
|
|
25
|
-
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
fetchPlanets()
|
|
26
39
|
}, [])
|
|
27
40
|
|
|
41
|
+
const resetForm = () => {
|
|
42
|
+
setName("")
|
|
43
|
+
setDescription("")
|
|
44
|
+
setDistance("0")
|
|
45
|
+
setDiameter("0")
|
|
46
|
+
setEditingId(null)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const handleAdd = async () => {
|
|
50
|
+
const toastId = toast.loading("Adding planet...")
|
|
51
|
+
try {
|
|
52
|
+
await orpc.createPlanet({
|
|
53
|
+
name,
|
|
54
|
+
description,
|
|
55
|
+
distanceFromSun: parseFloat(distance),
|
|
56
|
+
diameter: parseFloat(diameter),
|
|
57
|
+
hasRings: false,
|
|
58
|
+
})
|
|
59
|
+
await fetchPlanets()
|
|
60
|
+
resetForm()
|
|
61
|
+
toast.success("Planet added successfully", { id: toastId })
|
|
62
|
+
} catch (err: any) {
|
|
63
|
+
console.error(err)
|
|
64
|
+
toast.error(err.message || "Failed to add planet", { id: toastId })
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const handleUpdate = async (id: number) => {
|
|
69
|
+
const toastId = toast.loading("Updating planet...")
|
|
70
|
+
try {
|
|
71
|
+
await orpc.updatePlanet({
|
|
72
|
+
id,
|
|
73
|
+
name,
|
|
74
|
+
description,
|
|
75
|
+
distanceFromSun: parseFloat(distance),
|
|
76
|
+
diameter: parseFloat(diameter),
|
|
77
|
+
hasRings: false,
|
|
78
|
+
})
|
|
79
|
+
await fetchPlanets()
|
|
80
|
+
resetForm()
|
|
81
|
+
toast.success("Planet updated successfully", { id: toastId })
|
|
82
|
+
} catch (err: any) {
|
|
83
|
+
console.error(err)
|
|
84
|
+
toast.error(err.message || "Failed to update planet", { id: toastId })
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const handleDelete = async (id: number) => {
|
|
89
|
+
if (!confirm("Are you sure you want to delete this planet?")) return
|
|
90
|
+
const toastId = toast.loading("Deleting planet...")
|
|
91
|
+
try {
|
|
92
|
+
await orpc.deletePlanet({ id })
|
|
93
|
+
await fetchPlanets()
|
|
94
|
+
toast.success("Planet deleted successfully", { id: toastId })
|
|
95
|
+
} catch (err: any) {
|
|
96
|
+
console.error(err)
|
|
97
|
+
toast.error(err.message || "Failed to delete planet", { id: toastId })
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const startEdit = (planet: any) => {
|
|
102
|
+
setEditingId(planet.id)
|
|
103
|
+
setName(planet.name)
|
|
104
|
+
setDescription(planet.description || "")
|
|
105
|
+
setDistance(planet.distanceFromSun.toString())
|
|
106
|
+
setDiameter(planet.diameter.toString())
|
|
107
|
+
}
|
|
108
|
+
|
|
28
109
|
return (
|
|
29
|
-
<div className="flex flex-col gap-
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
110
|
+
<div className="flex flex-col gap-8">
|
|
111
|
+
<div>
|
|
112
|
+
<h1 className="text-2xl font-bold mb-2">Client + oRPC CRUD</h1>
|
|
113
|
+
<p className="text-muted-foreground">Manage planets directly from the client using oRPC mutations.</p>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div className="rounded-lg border p-6 bg-muted/30">
|
|
117
|
+
<h2 className="font-semibold mb-4 flex items-center gap-2">
|
|
118
|
+
{editingId ? <Pencil className="h-4 w-4" /> : <Plus className="h-4 w-4" />}
|
|
119
|
+
{editingId ? "Edit Planet" : "Add New Planet"}
|
|
120
|
+
</h2>
|
|
121
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
122
|
+
<div className="space-y-2">
|
|
123
|
+
<label className="text-xs font-medium uppercase tracking-wider text-muted-foreground">Name</label>
|
|
124
|
+
<Input value={name} onChange={(e) => setName(e.target.value)} placeholder="Mars" />
|
|
125
|
+
</div>
|
|
126
|
+
<div className="space-y-2">
|
|
127
|
+
<label className="text-xs font-medium uppercase tracking-wider text-muted-foreground">Description</label>
|
|
128
|
+
<Input value={description} onChange={(e) => setDescription(e.target.value)} placeholder="The red planet" />
|
|
129
|
+
</div>
|
|
130
|
+
<div className="space-y-2">
|
|
131
|
+
<label className="text-xs font-medium uppercase tracking-wider text-muted-foreground">Distance (M km)</label>
|
|
132
|
+
<Input type="number" value={distance} onChange={(e) => setDistance(e.target.value)} />
|
|
133
|
+
</div>
|
|
134
|
+
<div className="space-y-2">
|
|
135
|
+
<label className="text-xs font-medium uppercase tracking-wider text-muted-foreground">Diameter (km)</label>
|
|
136
|
+
<Input type="number" value={diameter} onChange={(e) => setDiameter(e.target.value)} />
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
<div className="mt-6 flex gap-2">
|
|
140
|
+
{editingId ? (
|
|
141
|
+
<>
|
|
142
|
+
<Button onClick={() => handleUpdate(editingId)} className="flex items-center gap-2">
|
|
143
|
+
<Check className="h-4 w-4" /> Save Changes
|
|
144
|
+
</Button>
|
|
145
|
+
<Button variant="outline" onClick={resetForm}>
|
|
146
|
+
Cancel
|
|
147
|
+
</Button>
|
|
148
|
+
</>
|
|
149
|
+
) : (
|
|
150
|
+
<Button onClick={handleAdd} className="flex items-center gap-2">
|
|
151
|
+
<Plus className="h-4 w-4" /> Add Planet
|
|
152
|
+
</Button>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div className="space-y-4">
|
|
158
|
+
<h2 className="font-semibold text-lg">Current Planets</h2>
|
|
35
159
|
{loading ? (
|
|
36
|
-
<p>Loading...</p>
|
|
160
|
+
<p>Loading planets...</p>
|
|
161
|
+
) : planets.length === 0 ? (
|
|
162
|
+
<p className="text-gray-500 italic">No planets found.</p>
|
|
37
163
|
) : (
|
|
38
|
-
<
|
|
164
|
+
<div className="grid grid-cols-1 gap-3">
|
|
165
|
+
{planets.map((planet) => (
|
|
166
|
+
<div key={planet.id} className="flex items-center justify-between rounded-lg border p-4 bg-background shadow-sm hover:shadow-md transition-shadow">
|
|
167
|
+
<div className="flex-1">
|
|
168
|
+
<div className="flex items-center gap-2">
|
|
169
|
+
<span className="font-bold text-lg">{planet.name}</span>
|
|
170
|
+
<span className="text-xs bg-muted px-2 py-0.5 rounded-full text-muted-foreground">ID: {planet.id}</span>
|
|
171
|
+
</div>
|
|
172
|
+
<p className="text-sm text-muted-foreground">{planet.description || "No description provided."}</p>
|
|
173
|
+
<div className="mt-2 flex gap-4 text-xs text-muted-foreground font-mono">
|
|
174
|
+
<span>Distance: {planet.distanceFromSun} M km</span>
|
|
175
|
+
<span>Diameter: {planet.diameter} km</span>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
<div className="flex gap-2 ml-4">
|
|
179
|
+
<Button variant="outline" size="icon" onClick={() => startEdit(planet)}>
|
|
180
|
+
<Pencil className="h-4 w-4" />
|
|
181
|
+
</Button>
|
|
182
|
+
<Button variant="destructive" size="icon" onClick={() => handleDelete(planet.id)}>
|
|
183
|
+
<Trash2 className="h-4 w-4" />
|
|
184
|
+
</Button>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
))}
|
|
188
|
+
</div>
|
|
39
189
|
)}
|
|
40
190
|
</div>
|
|
41
191
|
</div>
|
|
@@ -13,6 +13,10 @@ export const Route = createFileRoute("/isr")({
|
|
|
13
13
|
message: "This page is an example of ISR. In a production build with proper configuration, this data would be cached and updated in the background.",
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
|
+
headers: () => ({
|
|
17
|
+
// Cache at CDN for 10 seconds, allow stale content for up to 1 minute
|
|
18
|
+
"Cache-Control": "public, max-age=10, s-maxage=10, stale-while-revalidate=60",
|
|
19
|
+
}),
|
|
16
20
|
component: ISRExample,
|
|
17
21
|
})
|
|
18
22
|
|
|
@@ -1,35 +1,189 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { createFileRoute, useRouter } from "@tanstack/react-router"
|
|
3
|
+
import { Check, Pencil, Plus, Trash2 } from "lucide-react"
|
|
4
|
+
import { toast } from "sonner"
|
|
5
|
+
|
|
6
|
+
import { Button } from "@workspace/ui/components/button"
|
|
7
|
+
import { Input } from "@workspace/ui/components/input"
|
|
8
|
+
|
|
2
9
|
import { orpc } from "../lib/orpc"
|
|
3
10
|
|
|
4
11
|
export const Route = createFileRoute("/ssr-orpc")({
|
|
5
12
|
loader: async () => {
|
|
6
|
-
const data = await orpc.hello({ name: "SSR User" })
|
|
7
13
|
const planets = await orpc.getPlanets()
|
|
8
|
-
return {
|
|
14
|
+
return { planets }
|
|
9
15
|
},
|
|
10
16
|
component: SSRORPC,
|
|
11
17
|
})
|
|
12
18
|
|
|
13
19
|
function SSRORPC() {
|
|
14
|
-
const {
|
|
20
|
+
const { planets } = Route.useLoaderData()
|
|
21
|
+
const router = useRouter()
|
|
22
|
+
const [editingId, setEditingId] = React.useState<number | null>(null)
|
|
23
|
+
|
|
24
|
+
// Form states
|
|
25
|
+
const [name, setName] = React.useState("")
|
|
26
|
+
const [description, setDescription] = React.useState("")
|
|
27
|
+
const [distance, setDistance] = React.useState("0")
|
|
28
|
+
const [diameter, setDiameter] = React.useState("0")
|
|
29
|
+
const [isSubmitting, setIsSubmitting] = React.useState(false)
|
|
30
|
+
|
|
31
|
+
const resetForm = () => {
|
|
32
|
+
setName("")
|
|
33
|
+
setDescription("")
|
|
34
|
+
setDistance("0")
|
|
35
|
+
setDiameter("0")
|
|
36
|
+
setEditingId(null)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const handleAdd = async () => {
|
|
40
|
+
setIsSubmitting(true)
|
|
41
|
+
const toastId = toast.loading("Adding planet...")
|
|
42
|
+
try {
|
|
43
|
+
await orpc.createPlanet({
|
|
44
|
+
name,
|
|
45
|
+
description,
|
|
46
|
+
distanceFromSun: parseFloat(distance),
|
|
47
|
+
diameter: parseFloat(diameter),
|
|
48
|
+
hasRings: false,
|
|
49
|
+
})
|
|
50
|
+
await router.invalidate()
|
|
51
|
+
resetForm()
|
|
52
|
+
toast.success("Planet added successfully", { id: toastId })
|
|
53
|
+
} catch (err: any) {
|
|
54
|
+
console.error(err)
|
|
55
|
+
toast.error(err.message || "Failed to add planet", { id: toastId })
|
|
56
|
+
} finally {
|
|
57
|
+
setIsSubmitting(false)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const handleUpdate = async (id: number) => {
|
|
62
|
+
setIsSubmitting(true)
|
|
63
|
+
const toastId = toast.loading("Updating planet...")
|
|
64
|
+
try {
|
|
65
|
+
await orpc.updatePlanet({
|
|
66
|
+
id,
|
|
67
|
+
name,
|
|
68
|
+
description,
|
|
69
|
+
distanceFromSun: parseFloat(distance),
|
|
70
|
+
diameter: parseFloat(diameter),
|
|
71
|
+
hasRings: false,
|
|
72
|
+
})
|
|
73
|
+
await router.invalidate()
|
|
74
|
+
resetForm()
|
|
75
|
+
toast.success("Planet updated successfully", { id: toastId })
|
|
76
|
+
} catch (err: any) {
|
|
77
|
+
console.error(err)
|
|
78
|
+
toast.error(err.message || "Failed to update planet", { id: toastId })
|
|
79
|
+
} finally {
|
|
80
|
+
setIsSubmitting(false)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const handleDelete = async (id: number) => {
|
|
85
|
+
if (!confirm("Are you sure you want to delete this planet?")) return
|
|
86
|
+
setIsSubmitting(true)
|
|
87
|
+
const toastId = toast.loading("Deleting planet...")
|
|
88
|
+
try {
|
|
89
|
+
await orpc.deletePlanet({ id })
|
|
90
|
+
await router.invalidate()
|
|
91
|
+
toast.success("Planet deleted successfully", { id: toastId })
|
|
92
|
+
} catch (err: any) {
|
|
93
|
+
console.error(err)
|
|
94
|
+
toast.error(err.message || "Failed to delete planet", { id: toastId })
|
|
95
|
+
} finally {
|
|
96
|
+
setIsSubmitting(false)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const startEdit = (planet: any) => {
|
|
101
|
+
setEditingId(planet.id)
|
|
102
|
+
setName(planet.name)
|
|
103
|
+
setDescription(planet.description || "")
|
|
104
|
+
setDistance(planet.distanceFromSun.toString())
|
|
105
|
+
setDiameter(planet.diameter.toString())
|
|
106
|
+
}
|
|
15
107
|
|
|
16
108
|
return (
|
|
17
|
-
<div className="flex flex-col gap-
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
109
|
+
<div className="flex flex-col gap-8">
|
|
110
|
+
<div>
|
|
111
|
+
<h1 className="text-2xl font-bold mb-2">SSR + oRPC CRUD</h1>
|
|
112
|
+
<p className="text-muted-foreground">Manage planets using SSR loaders for fetching and oRPC mutations for actions.</p>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div className="rounded-lg border p-6 bg-muted/30">
|
|
116
|
+
<h2 className="font-semibold mb-4 flex items-center gap-2">
|
|
117
|
+
{editingId ? <Pencil className="h-4 w-4" /> : <Plus className="h-4 w-4" />}
|
|
118
|
+
{editingId ? "Edit Planet (SSR)" : "Add New Planet (SSR)"}
|
|
119
|
+
</h2>
|
|
120
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
121
|
+
<div className="space-y-2">
|
|
122
|
+
<label className="text-xs font-medium uppercase tracking-wider text-muted-foreground">Name</label>
|
|
123
|
+
<Input value={name} onChange={(e) => setName(e.target.value)} placeholder="Mars" disabled={isSubmitting} />
|
|
124
|
+
</div>
|
|
125
|
+
<div className="space-y-2">
|
|
126
|
+
<label className="text-xs font-medium uppercase tracking-wider text-muted-foreground">Description</label>
|
|
127
|
+
<Input value={description} onChange={(e) => setDescription(e.target.value)} placeholder="The red planet" disabled={isSubmitting} />
|
|
128
|
+
</div>
|
|
129
|
+
<div className="space-y-2">
|
|
130
|
+
<label className="text-xs font-medium uppercase tracking-wider text-muted-foreground">Distance (M km)</label>
|
|
131
|
+
<Input type="number" value={distance} onChange={(e) => setDistance(e.target.value)} disabled={isSubmitting} />
|
|
132
|
+
</div>
|
|
133
|
+
<div className="space-y-2">
|
|
134
|
+
<label className="text-xs font-medium uppercase tracking-wider text-muted-foreground">Diameter (km)</label>
|
|
135
|
+
<Input type="number" value={diameter} onChange={(e) => setDiameter(e.target.value)} disabled={isSubmitting} />
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
<div className="mt-6 flex gap-2">
|
|
139
|
+
{editingId ? (
|
|
140
|
+
<>
|
|
141
|
+
<Button onClick={() => handleUpdate(editingId)} className="flex items-center gap-2" disabled={isSubmitting}>
|
|
142
|
+
<Check className="h-4 w-4" /> {isSubmitting ? "Saving..." : "Save Changes"}
|
|
143
|
+
</Button>
|
|
144
|
+
<Button variant="outline" onClick={resetForm} disabled={isSubmitting}>
|
|
145
|
+
Cancel
|
|
146
|
+
</Button>
|
|
147
|
+
</>
|
|
148
|
+
) : (
|
|
149
|
+
<Button onClick={handleAdd} className="flex items-center gap-2" disabled={isSubmitting}>
|
|
150
|
+
<Plus className="h-4 w-4" /> {isSubmitting ? "Adding..." : "Add Planet"}
|
|
151
|
+
</Button>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
24
154
|
</div>
|
|
25
155
|
|
|
26
|
-
<div className="
|
|
27
|
-
<h2 className="font-semibold">Planets
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
156
|
+
<div className="space-y-4">
|
|
157
|
+
<h2 className="font-semibold text-lg">Current Planets (SSR Fetched)</h2>
|
|
158
|
+
{planets.length === 0 ? (
|
|
159
|
+
<p className="text-gray-500 italic">No planets found in the database.</p>
|
|
160
|
+
) : (
|
|
161
|
+
<div className="grid grid-cols-1 gap-3">
|
|
162
|
+
{planets.map((planet) => (
|
|
163
|
+
<div key={planet.id} className="flex items-center justify-between rounded-lg border p-4 bg-background shadow-sm hover:shadow-md transition-shadow">
|
|
164
|
+
<div className="flex-1">
|
|
165
|
+
<div className="flex items-center gap-2">
|
|
166
|
+
<span className="font-bold text-lg">{planet.name}</span>
|
|
167
|
+
<span className="text-xs bg-muted px-2 py-0.5 rounded-full text-muted-foreground">ID: {planet.id}</span>
|
|
168
|
+
</div>
|
|
169
|
+
<p className="text-sm text-muted-foreground">{planet.description || "No description provided."}</p>
|
|
170
|
+
<div className="mt-2 flex gap-4 text-xs text-muted-foreground font-mono">
|
|
171
|
+
<span>Distance: {planet.distanceFromSun} M km</span>
|
|
172
|
+
<span>Diameter: {planet.diameter} km</span>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
<div className="flex gap-2 ml-4">
|
|
176
|
+
<Button variant="outline" size="icon" onClick={() => startEdit(planet)} disabled={isSubmitting}>
|
|
177
|
+
<Pencil className="h-4 w-4" />
|
|
178
|
+
</Button>
|
|
179
|
+
<Button variant="destructive" size="icon" onClick={() => handleDelete(planet.id)} disabled={isSubmitting}>
|
|
180
|
+
<Trash2 className="h-4 w-4" />
|
|
181
|
+
</Button>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
))}
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
33
187
|
</div>
|
|
34
188
|
</div>
|
|
35
189
|
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ORPCError, os } from '@orpc/server'
|
|
2
2
|
import { db, schema } from '@workspace/db'
|
|
3
|
+
import { eq } from 'drizzle-orm'
|
|
3
4
|
import { z } from 'zod'
|
|
4
5
|
import type { Session } from '@workspace/auth/lib/auth'
|
|
5
6
|
|
|
@@ -47,6 +48,68 @@ export const router = o.router({
|
|
|
47
48
|
email: context.session.user.email,
|
|
48
49
|
}
|
|
49
50
|
}),
|
|
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
|
+
}),
|
|
50
113
|
})
|
|
51
114
|
|
|
52
115
|
export type AppRouter = typeof router
|