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.
- package/package.json +1 -1
- package/templates/base/AGENTS.md +203 -0
- package/templates/base/apps/server/AGENTS.md +64 -8
- package/templates/base/apps/web/AGENTS.md +82 -8
- package/templates/base/apps/web/src/components/KuckitModuleRoute.tsx +119 -0
- package/templates/base/apps/web/src/components/dashboard/app-sidebar.tsx +120 -0
- package/templates/base/apps/web/src/components/dashboard/dashboard-layout.tsx +46 -0
- package/templates/base/apps/web/src/components/dashboard/dashboard-overview.tsx +24 -0
- package/templates/base/apps/web/src/components/dashboard/index.ts +2 -0
- package/templates/base/apps/web/src/components/dashboard/nav-user.tsx +77 -0
- package/templates/base/apps/web/src/components/ui/avatar.tsx +39 -0
- package/templates/base/apps/web/src/components/ui/breadcrumb.tsx +102 -0
- package/templates/base/apps/web/src/components/ui/collapsible.tsx +21 -0
- package/templates/base/apps/web/src/components/ui/separator.tsx +26 -0
- package/templates/base/apps/web/src/components/ui/sheet.tsx +130 -0
- package/templates/base/apps/web/src/components/ui/sidebar.tsx +694 -0
- package/templates/base/apps/web/src/components/ui/skeleton.tsx +13 -0
- package/templates/base/apps/web/src/components/ui/tooltip.tsx +55 -0
- package/templates/base/apps/web/src/hooks/use-mobile.ts +19 -0
- package/templates/base/apps/web/src/lib/utils.ts +6 -0
- package/templates/base/apps/web/src/modules.client.ts +4 -3
- package/templates/base/apps/web/src/providers/KuckitProvider.tsx +1 -25
- package/templates/base/apps/web/src/routes/$.tsx +14 -0
- package/templates/base/apps/web/src/routes/dashboard/$.tsx +9 -0
- package/templates/base/apps/web/src/routes/dashboard/index.tsx +6 -0
- package/templates/base/apps/web/src/routes/dashboard.tsx +25 -0
- package/templates/base/apps/web/tsconfig.json +5 -1
- package/templates/base/apps/web/vite.config.ts +6 -0
- package/templates/base/packages/api/AGENTS.md +44 -5
- package/templates/base/packages/auth/AGENTS.md +17 -1
- package/templates/base/packages/db/AGENTS.md +16 -1
- package/templates/base/packages/items-module/AGENTS.md +99 -1
- package/templates/base/packages/items-module/src/ui/ItemsPage.tsx +50 -68
- 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
|
+
}
|
|
@@ -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
|
-
//
|
|
11
|
-
|
|
12
|
-
//
|
|
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,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
|
+
}
|
|
@@ -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
|
|
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
|
-
##
|
|
16
|
+
## Procedure Types
|
|
17
17
|
|
|
18
18
|
```typescript
|
|
19
|
-
import {
|
|
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
|
-
- `
|
|
27
|
-
- `
|
|
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
|
|
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
|
|
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
|
-
>
|
|
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
|
|
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
|
|
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
|
|
31
|
+
const {
|
|
32
|
+
data: items = [],
|
|
33
|
+
isLoading,
|
|
34
|
+
error,
|
|
35
|
+
} = useQuery({
|
|
36
|
+
queryKey: ['items'],
|
|
37
|
+
queryFn: () => rpc.items.list({}),
|
|
38
|
+
})
|
|
23
39
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
69
|
-
setError('Failed to create item')
|
|
70
|
-
}
|
|
62
|
+
)
|
|
71
63
|
}
|
|
72
64
|
|
|
73
|
-
const deleteItem =
|
|
74
|
-
|
|
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 (
|
|
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
|
|
124
|
-
|
|
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
|
-
}
|