@questpie/admin 0.0.1
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/.turbo/turbo-build.log +108 -0
- package/CHANGELOG.md +10 -0
- package/README.md +556 -0
- package/STATUS.md +917 -0
- package/VALIDATION.md +602 -0
- package/components.json +24 -0
- package/dist/__tests__/setup.mjs +38 -0
- package/dist/__tests__/test-utils.mjs +45 -0
- package/dist/__tests__/vitest.d.mjs +3 -0
- package/dist/components/admin-app.mjs +69 -0
- package/dist/components/fields/array-field.mjs +190 -0
- package/dist/components/fields/checkbox-field.mjs +34 -0
- package/dist/components/fields/custom-field.mjs +32 -0
- package/dist/components/fields/date-field.mjs +41 -0
- package/dist/components/fields/datetime-field.mjs +42 -0
- package/dist/components/fields/email-field.mjs +37 -0
- package/dist/components/fields/embedded-collection.mjs +253 -0
- package/dist/components/fields/field-types.mjs +1 -0
- package/dist/components/fields/field-utils.mjs +10 -0
- package/dist/components/fields/field-wrapper.mjs +34 -0
- package/dist/components/fields/index.mjs +23 -0
- package/dist/components/fields/json-field.mjs +243 -0
- package/dist/components/fields/locale-badge.mjs +16 -0
- package/dist/components/fields/number-field.mjs +39 -0
- package/dist/components/fields/password-field.mjs +37 -0
- package/dist/components/fields/relation-field.mjs +104 -0
- package/dist/components/fields/relation-picker.mjs +229 -0
- package/dist/components/fields/relation-select.mjs +188 -0
- package/dist/components/fields/rich-text-editor/index.mjs +897 -0
- package/dist/components/fields/select-field.mjs +41 -0
- package/dist/components/fields/switch-field.mjs +34 -0
- package/dist/components/fields/text-field.mjs +38 -0
- package/dist/components/fields/textarea-field.mjs +38 -0
- package/dist/components/index.mjs +59 -0
- package/dist/components/primitives/checkbox-input.mjs +127 -0
- package/dist/components/primitives/date-input.mjs +303 -0
- package/dist/components/primitives/index.mjs +12 -0
- package/dist/components/primitives/number-input.mjs +104 -0
- package/dist/components/primitives/select-input.mjs +177 -0
- package/dist/components/primitives/tag-input.mjs +135 -0
- package/dist/components/primitives/text-input.mjs +39 -0
- package/dist/components/primitives/textarea-input.mjs +37 -0
- package/dist/components/primitives/toggle-input.mjs +31 -0
- package/dist/components/primitives/types.mjs +12 -0
- package/dist/components/ui/accordion.mjs +55 -0
- package/dist/components/ui/avatar.mjs +54 -0
- package/dist/components/ui/badge.mjs +34 -0
- package/dist/components/ui/button.mjs +48 -0
- package/dist/components/ui/card.mjs +58 -0
- package/dist/components/ui/checkbox.mjs +21 -0
- package/dist/components/ui/combobox.mjs +163 -0
- package/dist/components/ui/dialog.mjs +95 -0
- package/dist/components/ui/dropdown-menu.mjs +138 -0
- package/dist/components/ui/field.mjs +113 -0
- package/dist/components/ui/input-group.mjs +82 -0
- package/dist/components/ui/input.mjs +17 -0
- package/dist/components/ui/label.mjs +15 -0
- package/dist/components/ui/popover.mjs +56 -0
- package/dist/components/ui/scroll-area.mjs +38 -0
- package/dist/components/ui/select.mjs +100 -0
- package/dist/components/ui/separator.mjs +16 -0
- package/dist/components/ui/sheet.mjs +90 -0
- package/dist/components/ui/sidebar.mjs +387 -0
- package/dist/components/ui/skeleton.mjs +14 -0
- package/dist/components/ui/spinner.mjs +16 -0
- package/dist/components/ui/switch.mjs +22 -0
- package/dist/components/ui/table.mjs +68 -0
- package/dist/components/ui/tabs.mjs +48 -0
- package/dist/components/ui/textarea.mjs +15 -0
- package/dist/components/ui/tooltip.mjs +44 -0
- package/dist/config/component-registry.mjs +38 -0
- package/dist/config/index.mjs +129 -0
- package/dist/hooks/admin-provider.mjs +70 -0
- package/dist/hooks/index.mjs +7 -0
- package/dist/hooks/store.mjs +178 -0
- package/dist/hooks/use-auth.mjs +76 -0
- package/dist/hooks/use-collection-db.mjs +146 -0
- package/dist/hooks/use-collection.mjs +112 -0
- package/dist/hooks/use-global.mjs +46 -0
- package/dist/hooks/use-mobile.mjs +20 -0
- package/dist/lib/utils.mjs +10 -0
- package/dist/styles/index.css +336 -0
- package/dist/styles/index.mjs +1 -0
- package/dist/utils/index.mjs +9 -0
- package/dist/views/auth/auth-layout.mjs +52 -0
- package/dist/views/auth/forgot-password-form.mjs +148 -0
- package/dist/views/auth/index.mjs +6 -0
- package/dist/views/auth/login-form.mjs +156 -0
- package/dist/views/auth/reset-password-form.mjs +184 -0
- package/dist/views/collection/auto-form-fields.mjs +525 -0
- package/dist/views/collection/collection-form.mjs +91 -0
- package/dist/views/collection/collection-list.mjs +76 -0
- package/dist/views/collection/form-field.mjs +42 -0
- package/dist/views/collection/index.mjs +6 -0
- package/dist/views/common/index.mjs +4 -0
- package/dist/views/common/locale-switcher.mjs +39 -0
- package/dist/views/common/version-history.mjs +272 -0
- package/dist/views/index.mjs +9 -0
- package/dist/views/layout/admin-layout.mjs +40 -0
- package/dist/views/layout/admin-router.mjs +95 -0
- package/dist/views/layout/admin-sidebar.mjs +63 -0
- package/dist/views/layout/index.mjs +5 -0
- package/package.json +276 -0
- package/src/__tests__/setup.ts +44 -0
- package/src/__tests__/test-utils.tsx +49 -0
- package/src/__tests__/vitest.d.ts +9 -0
- package/src/components/admin-app.tsx +221 -0
- package/src/components/fields/array-field.tsx +237 -0
- package/src/components/fields/checkbox-field.tsx +47 -0
- package/src/components/fields/custom-field.tsx +50 -0
- package/src/components/fields/date-field.tsx +65 -0
- package/src/components/fields/datetime-field.tsx +67 -0
- package/src/components/fields/email-field.tsx +51 -0
- package/src/components/fields/embedded-collection.tsx +315 -0
- package/src/components/fields/field-types.ts +162 -0
- package/src/components/fields/field-utils.ts +6 -0
- package/src/components/fields/field-wrapper.tsx +52 -0
- package/src/components/fields/index.ts +66 -0
- package/src/components/fields/json-field.tsx +440 -0
- package/src/components/fields/locale-badge.tsx +15 -0
- package/src/components/fields/number-field.tsx +57 -0
- package/src/components/fields/password-field.tsx +51 -0
- package/src/components/fields/relation-field.tsx +243 -0
- package/src/components/fields/relation-picker.tsx +402 -0
- package/src/components/fields/relation-select.tsx +327 -0
- package/src/components/fields/rich-text-editor/index.tsx +1337 -0
- package/src/components/fields/select-field.tsx +61 -0
- package/src/components/fields/switch-field.tsx +47 -0
- package/src/components/fields/text-field.tsx +55 -0
- package/src/components/fields/textarea-field.tsx +55 -0
- package/src/components/index.ts +40 -0
- package/src/components/primitives/checkbox-input.tsx +193 -0
- package/src/components/primitives/date-input.tsx +401 -0
- package/src/components/primitives/index.ts +24 -0
- package/src/components/primitives/number-input.tsx +132 -0
- package/src/components/primitives/select-input.tsx +296 -0
- package/src/components/primitives/tag-input.tsx +200 -0
- package/src/components/primitives/text-input.tsx +49 -0
- package/src/components/primitives/textarea-input.tsx +46 -0
- package/src/components/primitives/toggle-input.tsx +36 -0
- package/src/components/primitives/types.ts +235 -0
- package/src/components/ui/accordion.tsx +72 -0
- package/src/components/ui/avatar.tsx +106 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +53 -0
- package/src/components/ui/card.tsx +94 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/combobox.tsx +290 -0
- package/src/components/ui/dialog.tsx +151 -0
- package/src/components/ui/dropdown-menu.tsx +254 -0
- package/src/components/ui/field.tsx +227 -0
- package/src/components/ui/input-group.tsx +149 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/label.tsx +18 -0
- package/src/components/ui/popover.tsx +88 -0
- package/src/components/ui/scroll-area.tsx +53 -0
- package/src/components/ui/select.tsx +192 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +127 -0
- package/src/components/ui/sidebar.tsx +723 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/spinner.tsx +10 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/table.tsx +99 -0
- package/src/components/ui/tabs.tsx +82 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +70 -0
- package/src/config/component-registry.ts +190 -0
- package/src/config/index.ts +1099 -0
- package/src/hooks/README.md +269 -0
- package/src/hooks/admin-provider.tsx +110 -0
- package/src/hooks/index.ts +41 -0
- package/src/hooks/store.ts +248 -0
- package/src/hooks/use-auth.ts +168 -0
- package/src/hooks/use-collection-db.ts +209 -0
- package/src/hooks/use-collection.ts +156 -0
- package/src/hooks/use-global.ts +69 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/index.css +340 -0
- package/src/utils/index.ts +6 -0
- package/src/views/auth/auth-layout.tsx +77 -0
- package/src/views/auth/forgot-password-form.tsx +192 -0
- package/src/views/auth/index.ts +21 -0
- package/src/views/auth/login-form.tsx +229 -0
- package/src/views/auth/reset-password-form.tsx +232 -0
- package/src/views/collection/auto-form-fields.tsx +982 -0
- package/src/views/collection/collection-form.tsx +186 -0
- package/src/views/collection/collection-list.tsx +223 -0
- package/src/views/collection/form-field.tsx +52 -0
- package/src/views/collection/index.ts +15 -0
- package/src/views/common/index.ts +8 -0
- package/src/views/common/locale-switcher.tsx +45 -0
- package/src/views/common/version-history.tsx +406 -0
- package/src/views/index.ts +25 -0
- package/src/views/layout/admin-layout.tsx +117 -0
- package/src/views/layout/admin-router.tsx +206 -0
- package/src/views/layout/admin-sidebar.tsx +185 -0
- package/src/views/layout/index.ts +12 -0
- package/tsconfig.json +13 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsdown.config.ts +13 -0
- package/vitest.config.ts +29 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Admin Hooks - TanStack DB Integration
|
|
2
|
+
|
|
3
|
+
Admin hooks sú postavené na **TanStack DB Collections** pre offline-first, realtime data management.
|
|
4
|
+
|
|
5
|
+
## Výhody TanStack DB
|
|
6
|
+
|
|
7
|
+
✅ **Offline-first** - Data sú dostupné aj bez pripojenia
|
|
8
|
+
✅ **Optimistic updates** - Okamžitá UI odozva
|
|
9
|
+
✅ **Realtime sync** - Automatická synchronizácia cez SSE
|
|
10
|
+
✅ **Better DX** - Jednoduchšie API než query/mutation hooks
|
|
11
|
+
✅ **Built-in caching** - Inteligentné cache management
|
|
12
|
+
|
|
13
|
+
## Basic Usage
|
|
14
|
+
|
|
15
|
+
### 1. Setup AdminProvider
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { AdminProvider } from '@questpie/admin/hooks'
|
|
19
|
+
import { createQCMSClient } from '@questpie/cms/client'
|
|
20
|
+
import { QueryClient } from '@tanstack/react-query'
|
|
21
|
+
import type { cms } from './server/cms'
|
|
22
|
+
|
|
23
|
+
const client = createQCMSClient<typeof cms>({
|
|
24
|
+
baseURL: 'http://localhost:3000'
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const queryClient = new QueryClient()
|
|
28
|
+
|
|
29
|
+
function App() {
|
|
30
|
+
return (
|
|
31
|
+
<AdminProvider client={client} queryClient={queryClient}>
|
|
32
|
+
<YourAdminApp />
|
|
33
|
+
</AdminProvider>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Use Collection Hook
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { useCollection } from '@questpie/admin/hooks'
|
|
42
|
+
import type { cms } from './server/cms'
|
|
43
|
+
|
|
44
|
+
function PostsList() {
|
|
45
|
+
const posts = useCollection<typeof cms, 'posts'>('posts', {
|
|
46
|
+
// Optional: Add filters
|
|
47
|
+
baseFindOptions: {
|
|
48
|
+
where: { published: { eq: true } },
|
|
49
|
+
orderBy: { createdAt: 'desc' },
|
|
50
|
+
},
|
|
51
|
+
// Enable realtime sync
|
|
52
|
+
realtime: true,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div>
|
|
57
|
+
<h1>Posts ({posts.items.length})</h1>
|
|
58
|
+
{posts.items.map(post => (
|
|
59
|
+
<div key={post.id}>
|
|
60
|
+
<h2>{post.title}</h2>
|
|
61
|
+
<p>{post.content}</p>
|
|
62
|
+
</div>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 3. Create Item (Optimistic Update)
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import { useCollection } from '@questpie/admin/hooks'
|
|
73
|
+
|
|
74
|
+
function CreatePost() {
|
|
75
|
+
const posts = useCollection<typeof cms, 'posts'>('posts')
|
|
76
|
+
|
|
77
|
+
const handleCreate = async () => {
|
|
78
|
+
// Optimistic insert - UI updates immediately
|
|
79
|
+
await posts.insert({
|
|
80
|
+
title: 'New Post',
|
|
81
|
+
content: 'Hello World',
|
|
82
|
+
published: false,
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<button onClick={handleCreate}>
|
|
88
|
+
Create Post
|
|
89
|
+
</button>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 4. Update Item (Optimistic Update)
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import { useCollection, useCollectionItemById } from '@questpie/admin/hooks'
|
|
98
|
+
|
|
99
|
+
function EditPost({ id }: { id: string }) {
|
|
100
|
+
const posts = useCollection<typeof cms, 'posts'>('posts')
|
|
101
|
+
const post = useCollectionItemById(posts, id)
|
|
102
|
+
|
|
103
|
+
const handleUpdate = async () => {
|
|
104
|
+
if (!post) return
|
|
105
|
+
|
|
106
|
+
// Optimistic update - UI updates immediately
|
|
107
|
+
await posts.update(id, {
|
|
108
|
+
title: 'Updated Title',
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!post) return <div>Loading...</div>
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div>
|
|
116
|
+
<h1>{post.title}</h1>
|
|
117
|
+
<button onClick={handleUpdate}>Update Title</button>
|
|
118
|
+
</div>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 5. Delete Item (Optimistic Update)
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
import { useCollection } from '@questpie/admin/hooks'
|
|
127
|
+
|
|
128
|
+
function DeletePost({ id }: { id: string }) {
|
|
129
|
+
const posts = useCollection<typeof cms, 'posts'>('posts')
|
|
130
|
+
|
|
131
|
+
const handleDelete = async () => {
|
|
132
|
+
// Optimistic delete - UI updates immediately
|
|
133
|
+
await posts.delete(id)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<button onClick={handleDelete}>
|
|
138
|
+
Delete
|
|
139
|
+
</button>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Advanced Usage
|
|
145
|
+
|
|
146
|
+
### Realtime Sync with Custom Config
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
const posts = useCollection<typeof cms, 'posts'>('posts', {
|
|
150
|
+
realtime: {
|
|
151
|
+
enabled: true,
|
|
152
|
+
baseURL: 'http://localhost:3000',
|
|
153
|
+
basePath: '/cms',
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### With Relations (Eager Loading)
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
const posts = useCollection<typeof cms, 'posts'>('posts', {
|
|
162
|
+
baseFindOptions: {
|
|
163
|
+
with: {
|
|
164
|
+
author: true,
|
|
165
|
+
tags: true,
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// Now posts.items include author and tags data
|
|
171
|
+
posts.items.forEach(post => {
|
|
172
|
+
console.log(post.author?.name)
|
|
173
|
+
console.log(post.tags?.map(t => t.name))
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Filter & Sort
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
const posts = useCollection<typeof cms, 'posts'>('posts', {
|
|
181
|
+
baseFindOptions: {
|
|
182
|
+
where: {
|
|
183
|
+
AND: [
|
|
184
|
+
{ published: { eq: true } },
|
|
185
|
+
{ createdAt: { gte: new Date('2024-01-01') } },
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
orderBy: { createdAt: 'desc' },
|
|
189
|
+
limit: 10,
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Pagination with LoadSubset
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
import { useCollection } from '@questpie/admin/hooks'
|
|
198
|
+
|
|
199
|
+
function PaginatedPosts() {
|
|
200
|
+
const posts = useCollection<typeof cms, 'posts'>('posts')
|
|
201
|
+
|
|
202
|
+
const handleLoadMore = () => {
|
|
203
|
+
posts.loadSubset({
|
|
204
|
+
limit: 10,
|
|
205
|
+
offset: posts.items.length,
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<div>
|
|
211
|
+
{posts.items.map(post => (
|
|
212
|
+
<div key={post.id}>{post.title}</div>
|
|
213
|
+
))}
|
|
214
|
+
<button onClick={handleLoadMore}>Load More</button>
|
|
215
|
+
</div>
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## API Reference
|
|
221
|
+
|
|
222
|
+
### `useCollection(collectionName, options)`
|
|
223
|
+
|
|
224
|
+
Returns a TanStack DB Collection with full CRUD operations.
|
|
225
|
+
|
|
226
|
+
**Options:**
|
|
227
|
+
- `baseFindOptions` - Initial query (filters, sorting, relations)
|
|
228
|
+
- `realtime` - Enable SSE realtime sync (boolean or config object)
|
|
229
|
+
- `getKey` - Custom key extractor (defaults to `item.id`)
|
|
230
|
+
|
|
231
|
+
**Returns Collection with:**
|
|
232
|
+
- `items` - Array of collection items
|
|
233
|
+
- `insert(data)` - Create new item (optimistic)
|
|
234
|
+
- `update(id, data)` - Update item (optimistic)
|
|
235
|
+
- `delete(id)` - Delete item (optimistic)
|
|
236
|
+
- `loadSubset(options)` - Load more items with filters
|
|
237
|
+
- `replaceAll(items)` - Replace all items (for realtime)
|
|
238
|
+
|
|
239
|
+
### `useCollectionItemById(collection, id)`
|
|
240
|
+
|
|
241
|
+
Get single item from collection by ID (offline-first).
|
|
242
|
+
|
|
243
|
+
### `useCollectionInsert(collectionName)`
|
|
244
|
+
|
|
245
|
+
Get insert function for collection.
|
|
246
|
+
|
|
247
|
+
### `useCollectionUpdate(collectionName)`
|
|
248
|
+
|
|
249
|
+
Get update function for collection.
|
|
250
|
+
|
|
251
|
+
### `useCollectionDelete(collectionName)`
|
|
252
|
+
|
|
253
|
+
Get delete function for collection.
|
|
254
|
+
|
|
255
|
+
## Legacy Query/Mutation Hooks
|
|
256
|
+
|
|
257
|
+
For backward compatibility, query/mutation hooks are still available:
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
import {
|
|
261
|
+
useCollectionList,
|
|
262
|
+
useCollectionItem,
|
|
263
|
+
useCollectionCreate,
|
|
264
|
+
useCollectionUpdateMutation,
|
|
265
|
+
useCollectionDeleteMutation,
|
|
266
|
+
} from '@questpie/admin/hooks'
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Recommendation:** Use TanStack DB hooks (`useCollection`) for better DX and offline-first support.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import React, { createContext, useContext } from "react";
|
|
2
|
+
import type { QuestpieClient } from "questpie/client";
|
|
3
|
+
import type { Questpie } from "questpie";
|
|
4
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Admin context - provides access to CMS client
|
|
8
|
+
*/
|
|
9
|
+
export type AdminContext<T extends Questpie<any>> = {
|
|
10
|
+
client: QuestpieClient<T>;
|
|
11
|
+
locale?: string;
|
|
12
|
+
setLocale?: (locale: string) => void;
|
|
13
|
+
locales?: {
|
|
14
|
+
default: string;
|
|
15
|
+
available: string[];
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const AdminContextInstance = createContext<AdminContext<any> | null>(null);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Hook to access admin context
|
|
23
|
+
*/
|
|
24
|
+
export function useAdminContext<T extends Questpie<any>>(): AdminContext<T> {
|
|
25
|
+
const context = useContext(AdminContextInstance);
|
|
26
|
+
if (!context) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
"useAdminContext must be used within AdminProvider. Wrap your app with <AdminProvider client={client}>",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return context;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Admin provider props
|
|
36
|
+
*/
|
|
37
|
+
export type AdminProviderProps<T extends Questpie<any>> = {
|
|
38
|
+
client: QuestpieClient<T>;
|
|
39
|
+
queryClient?: QueryClient;
|
|
40
|
+
locales?: {
|
|
41
|
+
default: string;
|
|
42
|
+
available: string[];
|
|
43
|
+
};
|
|
44
|
+
children: React.ReactNode;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Admin provider - provides CMS client and query client to the app
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```tsx
|
|
52
|
+
* import { AdminProvider } from '@questpie/admin/hooks'
|
|
53
|
+
* import { createQuestpieClient } from 'questpie/client'
|
|
54
|
+
* import { QueryClient } from '@tanstack/react-query'
|
|
55
|
+
* import type { cms } from './server/cms'
|
|
56
|
+
*
|
|
57
|
+
* const client = createQuestpieClient<typeof cms>({
|
|
58
|
+
* baseURL: 'http://localhost:3000'
|
|
59
|
+
* })
|
|
60
|
+
*
|
|
61
|
+
* const queryClient = new QueryClient()
|
|
62
|
+
*
|
|
63
|
+
* function App() {
|
|
64
|
+
* return (
|
|
65
|
+
* <AdminProvider client={client} queryClient={queryClient}>
|
|
66
|
+
* <YourAdminApp />
|
|
67
|
+
* </AdminProvider>
|
|
68
|
+
* )
|
|
69
|
+
* }
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export function AdminProvider<T extends Questpie<any>>({
|
|
73
|
+
client,
|
|
74
|
+
queryClient,
|
|
75
|
+
locales,
|
|
76
|
+
children,
|
|
77
|
+
}: AdminProviderProps<T>): React.ReactElement {
|
|
78
|
+
const [defaultQueryClient] = React.useState(
|
|
79
|
+
() => queryClient ?? new QueryClient(),
|
|
80
|
+
);
|
|
81
|
+
const [locale, setLocale] = React.useState(() => {
|
|
82
|
+
if (locales?.default) return locales.default;
|
|
83
|
+
if (locales?.available?.length) return locales.available[0];
|
|
84
|
+
return typeof client.getLocale === "function"
|
|
85
|
+
? client.getLocale()
|
|
86
|
+
: undefined;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
React.useEffect(() => {
|
|
90
|
+
if (!locales?.available?.length) return;
|
|
91
|
+
if (!locale || !locales.available.includes(locale)) {
|
|
92
|
+
setLocale(locales.default ?? locales.available[0]);
|
|
93
|
+
}
|
|
94
|
+
}, [locale, locales]);
|
|
95
|
+
|
|
96
|
+
React.useEffect(() => {
|
|
97
|
+
if (typeof client.setLocale !== "function") return;
|
|
98
|
+
client.setLocale(locale);
|
|
99
|
+
}, [client, locale]);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<AdminContextInstance.Provider
|
|
103
|
+
value={{ client, locale, setLocale, locales }}
|
|
104
|
+
>
|
|
105
|
+
<QueryClientProvider client={defaultQueryClient}>
|
|
106
|
+
{children}
|
|
107
|
+
</QueryClientProvider>
|
|
108
|
+
</AdminContextInstance.Provider>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React hooks for admin package
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Provider and context
|
|
6
|
+
export { AdminProvider, useAdminContext } from "./admin-provider";
|
|
7
|
+
export type { AdminProviderProps, AdminContext } from "./admin-provider";
|
|
8
|
+
|
|
9
|
+
// Auth client (Better Auth integration)
|
|
10
|
+
export { createAdminAuthClient } from "./use-auth";
|
|
11
|
+
export type {
|
|
12
|
+
AdminAuthClient,
|
|
13
|
+
AdminAuthClientOptions,
|
|
14
|
+
AdminSession,
|
|
15
|
+
AdminUser,
|
|
16
|
+
} from "./use-auth";
|
|
17
|
+
|
|
18
|
+
// Jotai state management
|
|
19
|
+
// TODO: Add explicit types for isolatedDeclarations support
|
|
20
|
+
// export * from "./store";
|
|
21
|
+
|
|
22
|
+
// TanStack DB Collection hooks (recommended - offline-first, realtime)
|
|
23
|
+
export {
|
|
24
|
+
useCollection,
|
|
25
|
+
useCollectionItemById,
|
|
26
|
+
useCollectionInsert,
|
|
27
|
+
useCollectionUpdate,
|
|
28
|
+
useCollectionDelete,
|
|
29
|
+
} from "./use-collection-db";
|
|
30
|
+
|
|
31
|
+
// Legacy TanStack Query hooks (for backward compatibility)
|
|
32
|
+
export {
|
|
33
|
+
useCollectionList,
|
|
34
|
+
useCollectionItem,
|
|
35
|
+
useCollectionCreate,
|
|
36
|
+
useCollectionUpdate as useCollectionUpdateMutation,
|
|
37
|
+
useCollectionDelete as useCollectionDeleteMutation,
|
|
38
|
+
} from "./use-collection";
|
|
39
|
+
|
|
40
|
+
// Global hooks
|
|
41
|
+
export { useGlobal, useGlobalUpdate } from "./use-global";
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jotai Store for Admin State Management
|
|
3
|
+
*
|
|
4
|
+
* Centralized state atoms for the admin UI.
|
|
5
|
+
* Uses Jotai for reactive, atomic state management.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
|
|
9
|
+
import { atomWithStorage } from "jotai/utils";
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// UI State Atoms
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Sidebar collapsed state (persisted to localStorage)
|
|
17
|
+
*/
|
|
18
|
+
export const sidebarCollapsedAtom = atomWithStorage(
|
|
19
|
+
"admin:sidebar-collapsed",
|
|
20
|
+
false,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Current active locale
|
|
25
|
+
*/
|
|
26
|
+
export const localeAtom = atom<string | null>(null);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Available locales configuration
|
|
30
|
+
*/
|
|
31
|
+
export const localesConfigAtom = atom<{
|
|
32
|
+
default: string;
|
|
33
|
+
available: string[];
|
|
34
|
+
} | null>(null);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Mobile menu open state
|
|
38
|
+
*/
|
|
39
|
+
export const mobileMenuOpenAtom = atom(false);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Command palette open state
|
|
43
|
+
*/
|
|
44
|
+
export const commandPaletteOpenAtom = atom(false);
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Theme Atoms
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Theme mode (persisted to localStorage)
|
|
52
|
+
*/
|
|
53
|
+
export type ThemeMode = "light" | "dark" | "system";
|
|
54
|
+
|
|
55
|
+
export const themeModeAtom = atomWithStorage<ThemeMode>(
|
|
56
|
+
"admin:theme-mode",
|
|
57
|
+
"system",
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Derived atom for resolved theme (accounts for system preference)
|
|
62
|
+
*/
|
|
63
|
+
export const resolvedThemeAtom = atom((get) => {
|
|
64
|
+
const mode = get(themeModeAtom);
|
|
65
|
+
if (mode !== "system") return mode;
|
|
66
|
+
|
|
67
|
+
// Check system preference (this is a static check, won't react to changes)
|
|
68
|
+
if (typeof window !== "undefined") {
|
|
69
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
70
|
+
? "dark"
|
|
71
|
+
: "light";
|
|
72
|
+
}
|
|
73
|
+
return "light";
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// Navigation State Atoms
|
|
78
|
+
// ============================================================================
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Current active collection (for breadcrumbs, etc.)
|
|
82
|
+
*/
|
|
83
|
+
export const activeCollectionAtom = atom<string | null>(null);
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Current active item ID (for editing)
|
|
87
|
+
*/
|
|
88
|
+
export const activeItemIdAtom = atom<string | null>(null);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Navigation history stack
|
|
92
|
+
*/
|
|
93
|
+
export const navigationHistoryAtom = atom<string[]>([]);
|
|
94
|
+
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// Form State Atoms
|
|
97
|
+
// ============================================================================
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Global form dirty state (to warn on navigation)
|
|
101
|
+
*/
|
|
102
|
+
export const formDirtyAtom = atom(false);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Active form errors count
|
|
106
|
+
*/
|
|
107
|
+
export const formErrorsCountAtom = atom(0);
|
|
108
|
+
|
|
109
|
+
// ============================================================================
|
|
110
|
+
// Notification Atoms
|
|
111
|
+
// ============================================================================
|
|
112
|
+
|
|
113
|
+
export type NotificationType = "success" | "error" | "warning" | "info";
|
|
114
|
+
|
|
115
|
+
export type Notification = {
|
|
116
|
+
id: string;
|
|
117
|
+
type: NotificationType;
|
|
118
|
+
title: string;
|
|
119
|
+
message?: string;
|
|
120
|
+
duration?: number;
|
|
121
|
+
action?: {
|
|
122
|
+
label: string;
|
|
123
|
+
onClick: () => void;
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const notificationsAtom = atom<Notification[]>([]);
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Derived atom to add notification
|
|
131
|
+
*/
|
|
132
|
+
export const addNotificationAtom = atom(
|
|
133
|
+
null,
|
|
134
|
+
(get, set, notification: Omit<Notification, "id">) => {
|
|
135
|
+
const id = Math.random().toString(36).substring(2, 9);
|
|
136
|
+
const newNotification = { ...notification, id };
|
|
137
|
+
set(notificationsAtom, [...get(notificationsAtom), newNotification]);
|
|
138
|
+
|
|
139
|
+
// Auto-remove after duration
|
|
140
|
+
if (notification.duration !== 0) {
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
set(notificationsAtom, (prev) => prev.filter((n) => n.id !== id));
|
|
143
|
+
}, notification.duration ?? 5000);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return id;
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Derived atom to remove notification
|
|
152
|
+
*/
|
|
153
|
+
export const removeNotificationAtom = atom(null, (get, set, id: string) => {
|
|
154
|
+
set(notificationsAtom, (prev) => prev.filter((n) => n.id !== id));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ============================================================================
|
|
158
|
+
// Custom Hooks
|
|
159
|
+
// ============================================================================
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Hook for sidebar state
|
|
163
|
+
*/
|
|
164
|
+
export function useSidebar() {
|
|
165
|
+
const [collapsed, setCollapsed] = useAtom(sidebarCollapsedAtom);
|
|
166
|
+
return {
|
|
167
|
+
collapsed,
|
|
168
|
+
setCollapsed,
|
|
169
|
+
toggle: () => setCollapsed(!collapsed),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Hook for theme state
|
|
175
|
+
*/
|
|
176
|
+
export function useTheme() {
|
|
177
|
+
const [mode, setMode] = useAtom(themeModeAtom);
|
|
178
|
+
const resolved = useAtomValue(resolvedThemeAtom);
|
|
179
|
+
return {
|
|
180
|
+
mode,
|
|
181
|
+
setMode,
|
|
182
|
+
resolved,
|
|
183
|
+
isDark: resolved === "dark",
|
|
184
|
+
isLight: resolved === "light",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Hook for locale state
|
|
190
|
+
*/
|
|
191
|
+
export function useLocale() {
|
|
192
|
+
const [locale, setLocale] = useAtom(localeAtom);
|
|
193
|
+
const config = useAtomValue(localesConfigAtom);
|
|
194
|
+
return {
|
|
195
|
+
locale: locale ?? config?.default ?? null,
|
|
196
|
+
setLocale,
|
|
197
|
+
available: config?.available ?? [],
|
|
198
|
+
default: config?.default ?? null,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Hook for notifications
|
|
204
|
+
*/
|
|
205
|
+
export function useNotifications() {
|
|
206
|
+
const notifications = useAtomValue(notificationsAtom);
|
|
207
|
+
const addNotification = useSetAtom(addNotificationAtom);
|
|
208
|
+
const removeNotification = useSetAtom(removeNotificationAtom);
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
notifications,
|
|
212
|
+
add: addNotification,
|
|
213
|
+
remove: removeNotification,
|
|
214
|
+
success: (title: string, message?: string) =>
|
|
215
|
+
addNotification({ type: "success", title, message }),
|
|
216
|
+
error: (title: string, message?: string) =>
|
|
217
|
+
addNotification({ type: "error", title, message }),
|
|
218
|
+
warning: (title: string, message?: string) =>
|
|
219
|
+
addNotification({ type: "warning", title, message }),
|
|
220
|
+
info: (title: string, message?: string) =>
|
|
221
|
+
addNotification({ type: "info", title, message }),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Hook for command palette
|
|
227
|
+
*/
|
|
228
|
+
export function useCommandPalette() {
|
|
229
|
+
const [open, setOpen] = useAtom(commandPaletteOpenAtom);
|
|
230
|
+
return {
|
|
231
|
+
open,
|
|
232
|
+
setOpen,
|
|
233
|
+
toggle: () => setOpen(!open),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Hook for form dirty state
|
|
239
|
+
*/
|
|
240
|
+
export function useFormDirty() {
|
|
241
|
+
const [dirty, setDirty] = useAtom(formDirtyAtom);
|
|
242
|
+
return {
|
|
243
|
+
dirty,
|
|
244
|
+
setDirty,
|
|
245
|
+
markDirty: () => setDirty(true),
|
|
246
|
+
markClean: () => setDirty(false),
|
|
247
|
+
};
|
|
248
|
+
}
|