create-kuckit-app 0.2.0 → 0.3.0

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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/templates/base/AGENTS.md +203 -0
  3. package/templates/base/apps/server/AGENTS.md +64 -8
  4. package/templates/base/apps/web/AGENTS.md +82 -8
  5. package/templates/base/apps/web/src/components/KuckitModuleRoute.tsx +119 -0
  6. package/templates/base/apps/web/src/components/dashboard/app-sidebar.tsx +120 -0
  7. package/templates/base/apps/web/src/components/dashboard/dashboard-layout.tsx +46 -0
  8. package/templates/base/apps/web/src/components/dashboard/dashboard-overview.tsx +24 -0
  9. package/templates/base/apps/web/src/components/dashboard/index.ts +2 -0
  10. package/templates/base/apps/web/src/components/dashboard/nav-user.tsx +77 -0
  11. package/templates/base/apps/web/src/components/ui/avatar.tsx +39 -0
  12. package/templates/base/apps/web/src/components/ui/breadcrumb.tsx +102 -0
  13. package/templates/base/apps/web/src/components/ui/collapsible.tsx +21 -0
  14. package/templates/base/apps/web/src/components/ui/separator.tsx +26 -0
  15. package/templates/base/apps/web/src/components/ui/sheet.tsx +130 -0
  16. package/templates/base/apps/web/src/components/ui/sidebar.tsx +694 -0
  17. package/templates/base/apps/web/src/components/ui/skeleton.tsx +13 -0
  18. package/templates/base/apps/web/src/components/ui/tooltip.tsx +55 -0
  19. package/templates/base/apps/web/src/hooks/use-mobile.ts +19 -0
  20. package/templates/base/apps/web/src/lib/utils.ts +6 -0
  21. package/templates/base/apps/web/src/modules.client.ts +4 -3
  22. package/templates/base/apps/web/src/providers/KuckitProvider.tsx +1 -25
  23. package/templates/base/apps/web/src/routes/$.tsx +14 -0
  24. package/templates/base/apps/web/src/routes/dashboard/$.tsx +9 -0
  25. package/templates/base/apps/web/src/routes/dashboard/index.tsx +6 -0
  26. package/templates/base/apps/web/src/routes/dashboard.tsx +25 -0
  27. package/templates/base/apps/web/tsconfig.json +5 -1
  28. package/templates/base/apps/web/vite.config.ts +6 -0
  29. package/templates/base/packages/api/AGENTS.md +44 -5
  30. package/templates/base/packages/auth/AGENTS.md +17 -1
  31. package/templates/base/packages/db/AGENTS.md +16 -1
  32. package/templates/base/packages/items-module/AGENTS.md +99 -1
  33. package/templates/base/packages/items-module/src/ui/ItemsPage.tsx +50 -68
  34. package/templates/base/apps/web/src/lib/kuckit-router.ts +0 -42
@@ -0,0 +1,55 @@
1
+ import * as React from 'react'
2
+ import * as TooltipPrimitive from '@radix-ui/react-tooltip'
3
+
4
+ import { cn } from '@/lib/utils'
5
+
6
+ function TooltipProvider({
7
+ delayDuration = 0,
8
+ ...props
9
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
10
+ return (
11
+ <TooltipPrimitive.Provider
12
+ data-slot="tooltip-provider"
13
+ delayDuration={delayDuration}
14
+ {...props}
15
+ />
16
+ )
17
+ }
18
+
19
+ function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
20
+ return (
21
+ <TooltipProvider>
22
+ <TooltipPrimitive.Root data-slot="tooltip" {...props} />
23
+ </TooltipProvider>
24
+ )
25
+ }
26
+
27
+ function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
28
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
29
+ }
30
+
31
+ function TooltipContent({
32
+ className,
33
+ sideOffset = 0,
34
+ children,
35
+ ...props
36
+ }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
37
+ return (
38
+ <TooltipPrimitive.Portal>
39
+ <TooltipPrimitive.Content
40
+ data-slot="tooltip-content"
41
+ sideOffset={sideOffset}
42
+ className={cn(
43
+ 'bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
44
+ className
45
+ )}
46
+ {...props}
47
+ >
48
+ {children}
49
+ <TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
50
+ </TooltipPrimitive.Content>
51
+ </TooltipPrimitive.Portal>
52
+ )
53
+ }
54
+
55
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
@@ -0,0 +1,19 @@
1
+ import * as React from 'react'
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12
+ }
13
+ mql.addEventListener('change', onChange)
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15
+ return () => mql.removeEventListener('change', onChange)
16
+ }, [])
17
+
18
+ return !!isMobile
19
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ClientModuleSpec } from '@kuckit/sdk-react'
2
+ import { kuckitClientModule as itemsClientModule } from '@__APP_NAME_KEBAB__/items-module/src/client-module'
2
3
 
3
4
  /**
4
5
  * Client modules configuration
@@ -7,9 +8,9 @@ import type { ClientModuleSpec } from '@kuckit/sdk-react'
7
8
  */
8
9
  export const getClientModuleSpecs = (): ClientModuleSpec[] => {
9
10
  const modules: ClientModuleSpec[] = [
10
- // KUCKIT_MODULES_START
11
- // Client modules will be added here
12
- // KUCKIT_MODULES_END
11
+ // KUCKIT_CLIENT_MODULES_START
12
+ { module: itemsClientModule },
13
+ // KUCKIT_CLIENT_MODULES_END
13
14
  ]
14
15
 
15
16
  return modules
@@ -1,10 +1,5 @@
1
1
  import { createContext, useContext, useEffect, useState, useMemo, type ReactNode } from 'react'
2
- import {
3
- RouterProvider,
4
- createRouter,
5
- createRoute,
6
- type RouteComponent,
7
- } from '@tanstack/react-router'
2
+ import { RouterProvider, createRouter } from '@tanstack/react-router'
8
3
  import {
9
4
  loadKuckitClientModules,
10
5
  KuckitNavProvider,
@@ -16,7 +11,6 @@ import {
16
11
  type SlotRegistry,
17
12
  } from '@kuckit/sdk-react'
18
13
  import { routeTree } from '../routeTree.gen'
19
- import { Route as rootRoute } from '../routes/__root'
20
14
  import { useServices } from './ServicesProvider'
21
15
  import { getClientModuleSpecs } from '../modules.client'
22
16
 
@@ -81,26 +75,8 @@ export function KuckitProvider({ children }: KuckitProviderProps) {
81
75
  }
82
76
  }, [orpc, queryClient])
83
77
 
84
- // Build router with module routes
85
78
  const router = useMemo(() => {
86
79
  if (!loadResult) return null
87
-
88
- const moduleRouteDefs = loadResult.routeRegistry.getAll()
89
- const moduleRoutes = moduleRouteDefs.map((routeDef) =>
90
- createRoute({
91
- getParentRoute: () => rootRoute,
92
- path: routeDef.path,
93
- component: routeDef.component as RouteComponent,
94
- })
95
- )
96
-
97
- if (moduleRoutes.length > 0) {
98
- console.log(
99
- `Loaded ${moduleRoutes.length} module routes:`,
100
- moduleRouteDefs.map((r) => r.path)
101
- )
102
- }
103
-
104
80
  return createRouter({
105
81
  routeTree,
106
82
  defaultPreload: 'intent',
@@ -0,0 +1,14 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { KuckitModuleRoute } from '@/components/KuckitModuleRoute'
3
+
4
+ /**
5
+ * Catch-all route for KuckitModule routes at root level.
6
+ *
7
+ * This route matches any path not handled by other file-based routes
8
+ * and uses the KuckitModuleRoute component to look up and render
9
+ * the appropriate module component from the RouteRegistry.
10
+ */
11
+ // @ts-expect-error - Route types are generated when dev server runs
12
+ export const Route = createFileRoute('/$')({
13
+ component: KuckitModuleRoute,
14
+ })
@@ -0,0 +1,9 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { KuckitModuleRoute } from '@/components/KuckitModuleRoute'
3
+
4
+ /**
5
+ * Catch-all route for KuckitModule routes under /dashboard.
6
+ */
7
+ export const Route = createFileRoute('/dashboard/$')({
8
+ component: KuckitModuleRoute,
9
+ })
@@ -0,0 +1,6 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { DashboardOverview } from '@/components/dashboard/dashboard-overview'
3
+
4
+ export const Route = createFileRoute('/dashboard/')({
5
+ component: DashboardOverview,
6
+ })
@@ -0,0 +1,25 @@
1
+ import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
2
+ import { DashboardLayout } from '@/components/dashboard/dashboard-layout'
3
+ import { createAuthClientService } from '../services/auth-client'
4
+
5
+ export const Route = createFileRoute('/dashboard')({
6
+ beforeLoad: async () => {
7
+ const authClient = createAuthClientService()
8
+ const session = await authClient.getSession()
9
+ if (!session.data) {
10
+ throw redirect({
11
+ to: '/login',
12
+ })
13
+ }
14
+ return { session }
15
+ },
16
+ component: DashboardLayoutRoute,
17
+ })
18
+
19
+ function DashboardLayoutRoute() {
20
+ return (
21
+ <DashboardLayout>
22
+ <Outlet />
23
+ </DashboardLayout>
24
+ )
25
+ }
@@ -3,7 +3,11 @@
3
3
  "compilerOptions": {
4
4
  "lib": ["DOM", "DOM.Iterable", "ESNext"],
5
5
  "jsx": "react-jsx",
6
- "noEmit": true
6
+ "noEmit": true,
7
+ "baseUrl": ".",
8
+ "paths": {
9
+ "@/*": ["./src/*"]
10
+ }
7
11
  },
8
12
  "include": ["src"]
9
13
  }
@@ -1,7 +1,13 @@
1
1
  import { defineConfig } from 'vite'
2
2
  import react from '@vitejs/plugin-react'
3
3
  import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
4
+ import path from 'path'
4
5
 
5
6
  export default defineConfig({
6
7
  plugins: [TanStackRouterVite(), react()],
8
+ resolve: {
9
+ alias: {
10
+ '@': path.resolve(__dirname, './src'),
11
+ },
12
+ },
7
13
  })
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md - API Package
2
2
 
3
- > See root [AGENTS.md](../../AGENTS.md) for project overview
3
+ > See root [AGENTS.md](../../AGENTS.md) for SDK Module System patterns
4
4
 
5
5
  ## Purpose
6
6
 
@@ -13,15 +13,54 @@ Shared API context, types, and procedure definitions for oRPC.
13
13
  | `context.ts` | Request context type definitions |
14
14
  | `index.ts` | Public exports |
15
15
 
16
- ## Usage
16
+ ## Procedure Types
17
17
 
18
18
  ```typescript
19
- import { type Context, protectedProcedure } from '@__APP_NAME_KEBAB__/api'
19
+ import { publicProcedure, protectedProcedure } from '@__APP_NAME_KEBAB__/api'
20
+
21
+ // Public - no authentication required
22
+ export const healthRouter = {
23
+ ping: publicProcedure.handler(() => 'pong'),
24
+ }
25
+
26
+ // Protected - requires authenticated user
27
+ export const itemsRouter = {
28
+ list: protectedProcedure.handler(async ({ context }) => {
29
+ const userId = context.session?.user?.id
30
+ // ...
31
+ }),
32
+ }
20
33
  ```
21
34
 
22
35
  ## Context Structure
23
36
 
24
37
  The API context provides:
25
38
 
26
- - `container` - Scoped DI container for the request
27
- - `user` - Authenticated user (when using `protectedProcedure`)
39
+ - `di` - Scoped DI container for the request
40
+ - `session` - Current user session (when using `protectedProcedure`)
41
+
42
+ ## Typing DI Access in Routers
43
+
44
+ Use a module-local interface to type `context.di.cradle`:
45
+
46
+ ```typescript
47
+ // In your module's router file
48
+ interface ItemsCradle {
49
+ itemRepository: ItemRepository
50
+ createItem: (input: CreateItemInput) => Promise<Item>
51
+ }
52
+
53
+ export const itemsRouter = {
54
+ create: protectedProcedure.input(createItemSchema).handler(async ({ input, context }) => {
55
+ const { createItem } = context.di.cradle as ItemsCradle
56
+ return createItem(input)
57
+ }),
58
+ }
59
+ ```
60
+
61
+ **Why module-local interfaces:**
62
+
63
+ - Type assertion stays within the module
64
+ - Module remains self-contained
65
+ - Server app doesn't need to know about module internals
66
+ - Scales to any number of modules without server changes
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md - Auth Package
2
2
 
3
- > See root [AGENTS.md](../../AGENTS.md) for project overview
3
+ > See root [AGENTS.md](../../AGENTS.md) for SDK Module System patterns
4
4
 
5
5
  ## Purpose
6
6
 
@@ -36,6 +36,22 @@ await authClient.signOut()
36
36
  const session = await authClient.getSession()
37
37
  ```
38
38
 
39
+ ## Session in oRPC Context
40
+
41
+ The current user session is available in `protectedProcedure` handlers:
42
+
43
+ ```typescript
44
+ import { protectedProcedure } from '@__APP_NAME_KEBAB__/api'
45
+
46
+ export const myRouter = {
47
+ getProfile: protectedProcedure.handler(async ({ context }) => {
48
+ const userId = context.session?.user?.id
49
+ const userEmail = context.session?.user?.email
50
+ // ... use session data
51
+ }),
52
+ }
53
+ ```
54
+
39
55
  ## Configuration
40
56
 
41
57
  Auth requires these environment variables:
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md - Database Package
2
2
 
3
- > See root [AGENTS.md](../../AGENTS.md) for project overview
3
+ > See root [AGENTS.md](../../AGENTS.md) for SDK Module System patterns
4
4
 
5
5
  ## Purpose
6
6
 
@@ -50,6 +50,21 @@ bun run db:studio
50
50
  bun run db:migrate
51
51
  ```
52
52
 
53
+ ## Module Schema Pattern
54
+
55
+ Modules can define their own schemas in `adapters/`:
56
+
57
+ ```typescript
58
+ // packages/items-module/src/adapters/items.schema.ts
59
+ export const itemsTable = pgTable('items', {
60
+ id: uuid('id').primaryKey().defaultRandom(),
61
+ userId: text('user_id').notNull(),
62
+ name: text('name').notNull(),
63
+ })
64
+ ```
65
+
66
+ The schema is imported by `drizzle.config.ts` for migration generation.
67
+
53
68
  ## Connection
54
69
 
55
70
  ```typescript
@@ -1,6 +1,9 @@
1
1
  # AGENTS.md - Items Module
2
2
 
3
- > See root [AGENTS.md](../../AGENTS.md) for project overview
3
+ > **SDK Documentation**: For detailed module patterns, see the root [AGENTS.md](../../AGENTS.md) SDK Module System section
4
+ >
5
+ > - Server modules: `defineKuckitModule()` from `@kuckit/sdk`
6
+ > - Client modules: `defineKuckitClientModule()` from `@kuckit/sdk-react`
4
7
 
5
8
  ## Purpose
6
9
 
@@ -110,3 +113,98 @@ export const kuckitModule = defineKuckitModule({
110
113
  6. Implement use cases
111
114
  7. Create API router
112
115
  8. Register in server's `config/modules.ts`
116
+
117
+ ## Registration in Apps
118
+
119
+ ### Server Registration
120
+
121
+ ```typescript
122
+ // apps/server/src/config/modules.ts
123
+ import { kuckitModule as itemsModule } from '@__APP_NAME_KEBAB__/items-module'
124
+
125
+ export const modules = [itemsModule]
126
+ ```
127
+
128
+ ### Client Registration
129
+
130
+ ```typescript
131
+ // apps/web/src/modules.client.ts
132
+ import { kuckitClientModule as itemsClient } from '@__APP_NAME_KEBAB__/items-module/client'
133
+
134
+ export const clientModules = [{ module: itemsClient }]
135
+ ```
136
+
137
+ ## Client Module Pattern
138
+
139
+ The client module registers routes, navigation items, and slots:
140
+
141
+ ```typescript
142
+ // client-module.ts
143
+ import { defineKuckitClientModule } from '@kuckit/sdk-react'
144
+ import { ItemsPage } from './ui/ItemsPage'
145
+
146
+ export const kuckitClientModule = defineKuckitClientModule({
147
+ id: 'items',
148
+ displayName: 'Items',
149
+
150
+ routes: [
151
+ {
152
+ id: 'items-page',
153
+ path: '/items',
154
+ component: ItemsPage,
155
+ meta: { requiresAuth: true },
156
+ },
157
+ ],
158
+
159
+ navItems: [
160
+ {
161
+ id: 'items-nav',
162
+ label: 'Items',
163
+ href: '/items',
164
+ order: 20,
165
+ },
166
+ ],
167
+ })
168
+ ```
169
+
170
+ ## Using useRpc in Components
171
+
172
+ Module components access the API via the `useRpc` hook:
173
+
174
+ ```typescript
175
+ // ui/ItemsPage.tsx
176
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
177
+ import { useRpc } from '@kuckit/sdk-react'
178
+
179
+ interface ItemsRpc {
180
+ items: {
181
+ list: (input: Record<string, never>) => Promise<Item[]>
182
+ create: (input: { name: string; description?: string }) => Promise<Item>
183
+ delete: (input: { id: string }) => Promise<void>
184
+ }
185
+ }
186
+
187
+ export function ItemsPage() {
188
+ const rpc = useRpc<ItemsRpc>()
189
+ const queryClient = useQueryClient()
190
+
191
+ const { data: items = [], isLoading } = useQuery({
192
+ queryKey: ['items'],
193
+ queryFn: () => rpc.items.list({}),
194
+ })
195
+
196
+ const createMutation = useMutation({
197
+ mutationFn: (data: { name: string }) => rpc.items.create(data),
198
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['items'] }),
199
+ })
200
+
201
+ const deleteMutation = useMutation({
202
+ mutationFn: (id: string) => rpc.items.delete({ id }),
203
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['items'] }),
204
+ })
205
+
206
+ // ... render items list with create/delete handlers
207
+ }
208
+ ```
209
+
210
+ **Important**: Never use `import.meta.env` directly in module components - use `useRpc()` instead. Module packages are bundled separately and don't have access to the host app's environment variables.
@@ -1,4 +1,6 @@
1
- import { useState, useEffect } from 'react'
1
+ import { useState } from 'react'
2
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
3
+ import { useRpc } from '@kuckit/sdk-react'
2
4
 
3
5
  interface Item {
4
6
  id: string
@@ -8,85 +10,63 @@ interface Item {
8
10
  updatedAt: Date
9
11
  }
10
12
 
13
+ interface ItemsRpc {
14
+ items: {
15
+ list: (input: Record<string, never>) => Promise<Item[]>
16
+ create: (input: { name: string; description?: string }) => Promise<Item>
17
+ delete: (input: { id: string }) => Promise<void>
18
+ }
19
+ }
20
+
11
21
  /**
12
22
  * Items page component
13
- * Demonstrates CRUD operations using the items module
23
+ * Demonstrates CRUD operations using the items module with useRpc and TanStack Query
14
24
  */
15
25
  export function ItemsPage() {
16
- const [items, setItems] = useState<Item[]>([])
26
+ const rpc = useRpc<ItemsRpc>()
27
+ const queryClient = useQueryClient()
17
28
  const [newItemName, setNewItemName] = useState('')
18
29
  const [newItemDescription, setNewItemDescription] = useState('')
19
- const [loading, setLoading] = useState(true)
20
- const [error, setError] = useState<string | null>(null)
21
30
 
22
- const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3000'
31
+ const {
32
+ data: items = [],
33
+ isLoading,
34
+ error,
35
+ } = useQuery({
36
+ queryKey: ['items'],
37
+ queryFn: () => rpc.items.list({}),
38
+ })
23
39
 
24
- // Fetch items on mount
25
- useEffect(() => {
26
- fetchItems()
27
- }, [])
40
+ const createMutation = useMutation({
41
+ mutationFn: (data: { name: string; description?: string }) => rpc.items.create(data),
42
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['items'] }),
43
+ })
28
44
 
29
- const fetchItems = async () => {
30
- try {
31
- setLoading(true)
32
- const response = await fetch(`${apiUrl}/rpc/items.list`, {
33
- method: 'POST',
34
- headers: { 'Content-Type': 'application/json' },
35
- credentials: 'include',
36
- body: JSON.stringify({}),
37
- })
38
- if (response.ok) {
39
- const data = await response.json()
40
- setItems(data)
41
- }
42
- } catch {
43
- setError('Failed to load items')
44
- } finally {
45
- setLoading(false)
46
- }
47
- }
45
+ const deleteMutation = useMutation({
46
+ mutationFn: (id: string) => rpc.items.delete({ id }),
47
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['items'] }),
48
+ })
48
49
 
49
50
  const createItem = async (e: React.FormEvent) => {
50
51
  e.preventDefault()
51
52
  if (!newItemName.trim()) return
52
53
 
53
- try {
54
- const response = await fetch(`${apiUrl}/rpc/items.create`, {
55
- method: 'POST',
56
- headers: { 'Content-Type': 'application/json' },
57
- credentials: 'include',
58
- body: JSON.stringify({
59
- name: newItemName,
60
- description: newItemDescription || undefined,
61
- }),
62
- })
63
- if (response.ok) {
64
- setNewItemName('')
65
- setNewItemDescription('')
66
- fetchItems()
54
+ createMutation.mutate(
55
+ { name: newItemName, description: newItemDescription || undefined },
56
+ {
57
+ onSuccess: () => {
58
+ setNewItemName('')
59
+ setNewItemDescription('')
60
+ },
67
61
  }
68
- } catch {
69
- setError('Failed to create item')
70
- }
62
+ )
71
63
  }
72
64
 
73
- const deleteItem = async (id: string) => {
74
- try {
75
- const response = await fetch(`${apiUrl}/rpc/items.delete`, {
76
- method: 'POST',
77
- headers: { 'Content-Type': 'application/json' },
78
- credentials: 'include',
79
- body: JSON.stringify({ id }),
80
- })
81
- if (response.ok) {
82
- fetchItems()
83
- }
84
- } catch {
85
- setError('Failed to delete item')
86
- }
65
+ const deleteItem = (id: string) => {
66
+ deleteMutation.mutate(id)
87
67
  }
88
68
 
89
- if (loading) {
69
+ if (isLoading) {
90
70
  return <div style={{ padding: '2rem' }}>Loading items...</div>
91
71
  }
92
72
 
@@ -94,12 +74,9 @@ export function ItemsPage() {
94
74
  <div style={{ padding: '2rem', fontFamily: 'system-ui', maxWidth: '600px', margin: '0 auto' }}>
95
75
  <h1>Items</h1>
96
76
 
97
- {error && (
77
+ {(error || createMutation.error || deleteMutation.error) && (
98
78
  <div style={{ color: 'red', marginBottom: '1rem' }}>
99
- {error}
100
- <button onClick={() => setError(null)} style={{ marginLeft: '1rem' }}>
101
- Dismiss
102
- </button>
79
+ {error?.message || createMutation.error?.message || deleteMutation.error?.message}
103
80
  </div>
104
81
  )}
105
82
 
@@ -120,8 +97,12 @@ export function ItemsPage() {
120
97
  onChange={(e) => setNewItemDescription(e.target.value)}
121
98
  style={{ padding: '0.5rem', fontSize: '1rem' }}
122
99
  />
123
- <button type="submit" style={{ padding: '0.5rem 1rem', cursor: 'pointer' }}>
124
- Add Item
100
+ <button
101
+ type="submit"
102
+ disabled={createMutation.isPending}
103
+ style={{ padding: '0.5rem 1rem', cursor: 'pointer' }}
104
+ >
105
+ {createMutation.isPending ? 'Adding...' : 'Add Item'}
125
106
  </button>
126
107
  </div>
127
108
  </form>
@@ -148,6 +129,7 @@ export function ItemsPage() {
148
129
  </div>
149
130
  <button
150
131
  onClick={() => deleteItem(item.id)}
132
+ disabled={deleteMutation.isPending}
151
133
  style={{ cursor: 'pointer', color: 'red', background: 'none', border: 'none' }}
152
134
  >
153
135
  Delete
@@ -1,42 +0,0 @@
1
- import { createRoute, type AnyRoute, type RouteComponent } from '@tanstack/react-router'
2
- import type { QueryClient } from '@tanstack/react-query'
3
- import type { RouteRegistry, RouteDefinition } from '@kuckit/sdk-react'
4
- import type { ORPCUtils } from '../services/types'
5
-
6
- export interface KuckitRouterContext {
7
- orpc: ORPCUtils
8
- queryClient: QueryClient
9
- }
10
-
11
- /**
12
- * Build TanStack Router routes from a RouteRegistry
13
- */
14
- export function buildModuleRoutes(routeRegistry: RouteRegistry, rootRoute: AnyRoute): AnyRoute[] {
15
- const routes = routeRegistry.getAll()
16
- const routeMap = new Map<string, AnyRoute>()
17
-
18
- for (const routeDef of routes) {
19
- const route = createModuleRoute(routeDef, rootRoute)
20
- routeMap.set(routeDef.id, route)
21
- }
22
-
23
- const topLevelRoutes = routes
24
- .filter((r) => !r.parentRouteId || r.parentRouteId === '__root__')
25
- .map((r) => routeMap.get(r.id)!)
26
- .filter(Boolean)
27
-
28
- return topLevelRoutes
29
- }
30
-
31
- function createModuleRoute(routeDef: RouteDefinition, parentRoute: AnyRoute): AnyRoute {
32
- return createRoute({
33
- getParentRoute: () => parentRoute,
34
- path: routeDef.path,
35
- component: routeDef.component as RouteComponent,
36
- ...(routeDef.meta?.title && {
37
- head: () => ({
38
- meta: [{ title: routeDef.meta!.title }],
39
- }),
40
- }),
41
- })
42
- }