@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.
Files changed (203) hide show
  1. package/.turbo/turbo-build.log +108 -0
  2. package/CHANGELOG.md +10 -0
  3. package/README.md +556 -0
  4. package/STATUS.md +917 -0
  5. package/VALIDATION.md +602 -0
  6. package/components.json +24 -0
  7. package/dist/__tests__/setup.mjs +38 -0
  8. package/dist/__tests__/test-utils.mjs +45 -0
  9. package/dist/__tests__/vitest.d.mjs +3 -0
  10. package/dist/components/admin-app.mjs +69 -0
  11. package/dist/components/fields/array-field.mjs +190 -0
  12. package/dist/components/fields/checkbox-field.mjs +34 -0
  13. package/dist/components/fields/custom-field.mjs +32 -0
  14. package/dist/components/fields/date-field.mjs +41 -0
  15. package/dist/components/fields/datetime-field.mjs +42 -0
  16. package/dist/components/fields/email-field.mjs +37 -0
  17. package/dist/components/fields/embedded-collection.mjs +253 -0
  18. package/dist/components/fields/field-types.mjs +1 -0
  19. package/dist/components/fields/field-utils.mjs +10 -0
  20. package/dist/components/fields/field-wrapper.mjs +34 -0
  21. package/dist/components/fields/index.mjs +23 -0
  22. package/dist/components/fields/json-field.mjs +243 -0
  23. package/dist/components/fields/locale-badge.mjs +16 -0
  24. package/dist/components/fields/number-field.mjs +39 -0
  25. package/dist/components/fields/password-field.mjs +37 -0
  26. package/dist/components/fields/relation-field.mjs +104 -0
  27. package/dist/components/fields/relation-picker.mjs +229 -0
  28. package/dist/components/fields/relation-select.mjs +188 -0
  29. package/dist/components/fields/rich-text-editor/index.mjs +897 -0
  30. package/dist/components/fields/select-field.mjs +41 -0
  31. package/dist/components/fields/switch-field.mjs +34 -0
  32. package/dist/components/fields/text-field.mjs +38 -0
  33. package/dist/components/fields/textarea-field.mjs +38 -0
  34. package/dist/components/index.mjs +59 -0
  35. package/dist/components/primitives/checkbox-input.mjs +127 -0
  36. package/dist/components/primitives/date-input.mjs +303 -0
  37. package/dist/components/primitives/index.mjs +12 -0
  38. package/dist/components/primitives/number-input.mjs +104 -0
  39. package/dist/components/primitives/select-input.mjs +177 -0
  40. package/dist/components/primitives/tag-input.mjs +135 -0
  41. package/dist/components/primitives/text-input.mjs +39 -0
  42. package/dist/components/primitives/textarea-input.mjs +37 -0
  43. package/dist/components/primitives/toggle-input.mjs +31 -0
  44. package/dist/components/primitives/types.mjs +12 -0
  45. package/dist/components/ui/accordion.mjs +55 -0
  46. package/dist/components/ui/avatar.mjs +54 -0
  47. package/dist/components/ui/badge.mjs +34 -0
  48. package/dist/components/ui/button.mjs +48 -0
  49. package/dist/components/ui/card.mjs +58 -0
  50. package/dist/components/ui/checkbox.mjs +21 -0
  51. package/dist/components/ui/combobox.mjs +163 -0
  52. package/dist/components/ui/dialog.mjs +95 -0
  53. package/dist/components/ui/dropdown-menu.mjs +138 -0
  54. package/dist/components/ui/field.mjs +113 -0
  55. package/dist/components/ui/input-group.mjs +82 -0
  56. package/dist/components/ui/input.mjs +17 -0
  57. package/dist/components/ui/label.mjs +15 -0
  58. package/dist/components/ui/popover.mjs +56 -0
  59. package/dist/components/ui/scroll-area.mjs +38 -0
  60. package/dist/components/ui/select.mjs +100 -0
  61. package/dist/components/ui/separator.mjs +16 -0
  62. package/dist/components/ui/sheet.mjs +90 -0
  63. package/dist/components/ui/sidebar.mjs +387 -0
  64. package/dist/components/ui/skeleton.mjs +14 -0
  65. package/dist/components/ui/spinner.mjs +16 -0
  66. package/dist/components/ui/switch.mjs +22 -0
  67. package/dist/components/ui/table.mjs +68 -0
  68. package/dist/components/ui/tabs.mjs +48 -0
  69. package/dist/components/ui/textarea.mjs +15 -0
  70. package/dist/components/ui/tooltip.mjs +44 -0
  71. package/dist/config/component-registry.mjs +38 -0
  72. package/dist/config/index.mjs +129 -0
  73. package/dist/hooks/admin-provider.mjs +70 -0
  74. package/dist/hooks/index.mjs +7 -0
  75. package/dist/hooks/store.mjs +178 -0
  76. package/dist/hooks/use-auth.mjs +76 -0
  77. package/dist/hooks/use-collection-db.mjs +146 -0
  78. package/dist/hooks/use-collection.mjs +112 -0
  79. package/dist/hooks/use-global.mjs +46 -0
  80. package/dist/hooks/use-mobile.mjs +20 -0
  81. package/dist/lib/utils.mjs +10 -0
  82. package/dist/styles/index.css +336 -0
  83. package/dist/styles/index.mjs +1 -0
  84. package/dist/utils/index.mjs +9 -0
  85. package/dist/views/auth/auth-layout.mjs +52 -0
  86. package/dist/views/auth/forgot-password-form.mjs +148 -0
  87. package/dist/views/auth/index.mjs +6 -0
  88. package/dist/views/auth/login-form.mjs +156 -0
  89. package/dist/views/auth/reset-password-form.mjs +184 -0
  90. package/dist/views/collection/auto-form-fields.mjs +525 -0
  91. package/dist/views/collection/collection-form.mjs +91 -0
  92. package/dist/views/collection/collection-list.mjs +76 -0
  93. package/dist/views/collection/form-field.mjs +42 -0
  94. package/dist/views/collection/index.mjs +6 -0
  95. package/dist/views/common/index.mjs +4 -0
  96. package/dist/views/common/locale-switcher.mjs +39 -0
  97. package/dist/views/common/version-history.mjs +272 -0
  98. package/dist/views/index.mjs +9 -0
  99. package/dist/views/layout/admin-layout.mjs +40 -0
  100. package/dist/views/layout/admin-router.mjs +95 -0
  101. package/dist/views/layout/admin-sidebar.mjs +63 -0
  102. package/dist/views/layout/index.mjs +5 -0
  103. package/package.json +276 -0
  104. package/src/__tests__/setup.ts +44 -0
  105. package/src/__tests__/test-utils.tsx +49 -0
  106. package/src/__tests__/vitest.d.ts +9 -0
  107. package/src/components/admin-app.tsx +221 -0
  108. package/src/components/fields/array-field.tsx +237 -0
  109. package/src/components/fields/checkbox-field.tsx +47 -0
  110. package/src/components/fields/custom-field.tsx +50 -0
  111. package/src/components/fields/date-field.tsx +65 -0
  112. package/src/components/fields/datetime-field.tsx +67 -0
  113. package/src/components/fields/email-field.tsx +51 -0
  114. package/src/components/fields/embedded-collection.tsx +315 -0
  115. package/src/components/fields/field-types.ts +162 -0
  116. package/src/components/fields/field-utils.ts +6 -0
  117. package/src/components/fields/field-wrapper.tsx +52 -0
  118. package/src/components/fields/index.ts +66 -0
  119. package/src/components/fields/json-field.tsx +440 -0
  120. package/src/components/fields/locale-badge.tsx +15 -0
  121. package/src/components/fields/number-field.tsx +57 -0
  122. package/src/components/fields/password-field.tsx +51 -0
  123. package/src/components/fields/relation-field.tsx +243 -0
  124. package/src/components/fields/relation-picker.tsx +402 -0
  125. package/src/components/fields/relation-select.tsx +327 -0
  126. package/src/components/fields/rich-text-editor/index.tsx +1337 -0
  127. package/src/components/fields/select-field.tsx +61 -0
  128. package/src/components/fields/switch-field.tsx +47 -0
  129. package/src/components/fields/text-field.tsx +55 -0
  130. package/src/components/fields/textarea-field.tsx +55 -0
  131. package/src/components/index.ts +40 -0
  132. package/src/components/primitives/checkbox-input.tsx +193 -0
  133. package/src/components/primitives/date-input.tsx +401 -0
  134. package/src/components/primitives/index.ts +24 -0
  135. package/src/components/primitives/number-input.tsx +132 -0
  136. package/src/components/primitives/select-input.tsx +296 -0
  137. package/src/components/primitives/tag-input.tsx +200 -0
  138. package/src/components/primitives/text-input.tsx +49 -0
  139. package/src/components/primitives/textarea-input.tsx +46 -0
  140. package/src/components/primitives/toggle-input.tsx +36 -0
  141. package/src/components/primitives/types.ts +235 -0
  142. package/src/components/ui/accordion.tsx +72 -0
  143. package/src/components/ui/avatar.tsx +106 -0
  144. package/src/components/ui/badge.tsx +48 -0
  145. package/src/components/ui/button.tsx +53 -0
  146. package/src/components/ui/card.tsx +94 -0
  147. package/src/components/ui/checkbox.tsx +27 -0
  148. package/src/components/ui/combobox.tsx +290 -0
  149. package/src/components/ui/dialog.tsx +151 -0
  150. package/src/components/ui/dropdown-menu.tsx +254 -0
  151. package/src/components/ui/field.tsx +227 -0
  152. package/src/components/ui/input-group.tsx +149 -0
  153. package/src/components/ui/input.tsx +20 -0
  154. package/src/components/ui/label.tsx +18 -0
  155. package/src/components/ui/popover.tsx +88 -0
  156. package/src/components/ui/scroll-area.tsx +53 -0
  157. package/src/components/ui/select.tsx +192 -0
  158. package/src/components/ui/separator.tsx +23 -0
  159. package/src/components/ui/sheet.tsx +127 -0
  160. package/src/components/ui/sidebar.tsx +723 -0
  161. package/src/components/ui/skeleton.tsx +13 -0
  162. package/src/components/ui/spinner.tsx +10 -0
  163. package/src/components/ui/switch.tsx +32 -0
  164. package/src/components/ui/table.tsx +99 -0
  165. package/src/components/ui/tabs.tsx +82 -0
  166. package/src/components/ui/textarea.tsx +18 -0
  167. package/src/components/ui/tooltip.tsx +70 -0
  168. package/src/config/component-registry.ts +190 -0
  169. package/src/config/index.ts +1099 -0
  170. package/src/hooks/README.md +269 -0
  171. package/src/hooks/admin-provider.tsx +110 -0
  172. package/src/hooks/index.ts +41 -0
  173. package/src/hooks/store.ts +248 -0
  174. package/src/hooks/use-auth.ts +168 -0
  175. package/src/hooks/use-collection-db.ts +209 -0
  176. package/src/hooks/use-collection.ts +156 -0
  177. package/src/hooks/use-global.ts +69 -0
  178. package/src/hooks/use-mobile.ts +21 -0
  179. package/src/lib/utils.ts +6 -0
  180. package/src/styles/index.css +340 -0
  181. package/src/utils/index.ts +6 -0
  182. package/src/views/auth/auth-layout.tsx +77 -0
  183. package/src/views/auth/forgot-password-form.tsx +192 -0
  184. package/src/views/auth/index.ts +21 -0
  185. package/src/views/auth/login-form.tsx +229 -0
  186. package/src/views/auth/reset-password-form.tsx +232 -0
  187. package/src/views/collection/auto-form-fields.tsx +982 -0
  188. package/src/views/collection/collection-form.tsx +186 -0
  189. package/src/views/collection/collection-list.tsx +223 -0
  190. package/src/views/collection/form-field.tsx +52 -0
  191. package/src/views/collection/index.ts +15 -0
  192. package/src/views/common/index.ts +8 -0
  193. package/src/views/common/locale-switcher.tsx +45 -0
  194. package/src/views/common/version-history.tsx +406 -0
  195. package/src/views/index.ts +25 -0
  196. package/src/views/layout/admin-layout.tsx +117 -0
  197. package/src/views/layout/admin-router.tsx +206 -0
  198. package/src/views/layout/admin-sidebar.tsx +185 -0
  199. package/src/views/layout/index.ts +12 -0
  200. package/tsconfig.json +13 -0
  201. package/tsconfig.tsbuildinfo +1 -0
  202. package/tsdown.config.ts +13 -0
  203. package/vitest.config.ts +29 -0
@@ -0,0 +1,129 @@
1
+ //#region src/config/index.ts
2
+ /**
3
+ * Define admin configuration with type inference
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import { defineAdminConfig } from '@questpie/admin/config'
8
+ * import type { cms } from './server/cms'
9
+ *
10
+ * export const admin = defineAdminConfig<typeof cms>()({
11
+ * app: {
12
+ * brand: { name: "My CMS" }
13
+ * },
14
+ * collections: {
15
+ * posts: {
16
+ * label: "Posts",
17
+ * icon: "posts"
18
+ * }
19
+ * }
20
+ * })
21
+ * ```
22
+ */
23
+ const defineAdminConfig = () => (config) => {
24
+ return config;
25
+ };
26
+ /**
27
+ * Define a single collection config (for modular configs)
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * // collections/posts.admin.ts
32
+ * import { defineCollectionConfig } from '@questpie/admin/config'
33
+ * import type { cms } from '../server/cms'
34
+ *
35
+ * export const postsAdminConfig = defineCollectionConfig<typeof cms, "posts">()({
36
+ * label: "Blog Posts",
37
+ * icon: "article",
38
+ * list: {
39
+ * defaultColumns: ["title", "status", "createdAt"],
40
+ * },
41
+ * edit: {
42
+ * sections: [
43
+ * { title: "Content", fields: ["title", "content"] },
44
+ * { title: "Settings", fields: ["status", "publishedAt"] },
45
+ * ],
46
+ * },
47
+ * })
48
+ * ```
49
+ */
50
+ const defineCollectionConfig = () => (config) => {
51
+ return config;
52
+ };
53
+ /**
54
+ * Merge multiple admin configs into one
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * const admin = defineAdminConfig<typeof cms>()(
59
+ * mergeAdminConfigs([
60
+ * baseConfig,
61
+ * postsConfig,
62
+ * productsConfig,
63
+ * ])
64
+ * )
65
+ * ```
66
+ */
67
+ function mergeAdminConfigs(configs) {
68
+ const result = { app: {} };
69
+ for (const config of configs) {
70
+ if (config.app) result.app = {
71
+ ...result.app,
72
+ ...config.app
73
+ };
74
+ if (config.collections) result.collections = {
75
+ ...result.collections,
76
+ ...config.collections
77
+ };
78
+ }
79
+ return result;
80
+ }
81
+ /**
82
+ * Define app config separately
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * export const appConfig = defineAppConfig({
87
+ * brand: { name: "My CMS" },
88
+ * locales: { default: "en", available: ["en", "sk"] },
89
+ * })
90
+ * ```
91
+ */
92
+ const defineAppConfig = (config) => {
93
+ return config;
94
+ };
95
+ /**
96
+ * Define dashboard config
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * export const dashboardConfig = defineDashboardConfig({
101
+ * widgets: [
102
+ * { id: "stats", type: "stats", position: { x: 0, y: 0, w: 4, h: 2 } },
103
+ * ],
104
+ * })
105
+ * ```
106
+ */
107
+ const defineDashboardConfig = (config) => {
108
+ return config;
109
+ };
110
+ /**
111
+ * Define views configuration
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * import { defineViewsConfig } from '@questpie/admin/config'
116
+ * import { CustomLoginForm } from './components/CustomLoginForm'
117
+ *
118
+ * export const viewsConfig = defineViewsConfig({
119
+ * LoginForm: CustomLoginForm,
120
+ * // Override any default view
121
+ * })
122
+ * ```
123
+ */
124
+ const defineViewsConfig = (config) => {
125
+ return config;
126
+ };
127
+
128
+ //#endregion
129
+ export { defineAdminConfig, defineAppConfig, defineCollectionConfig, defineDashboardConfig, defineViewsConfig, mergeAdminConfigs };
@@ -0,0 +1,70 @@
1
+ import React, { createContext, useContext } from "react";
2
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3
+ import { jsx } from "react/jsx-runtime";
4
+
5
+ //#region src/hooks/admin-provider.tsx
6
+ const AdminContextInstance = createContext(null);
7
+ /**
8
+ * Hook to access admin context
9
+ */
10
+ function useAdminContext() {
11
+ const context = useContext(AdminContextInstance);
12
+ if (!context) throw new Error("useAdminContext must be used within AdminProvider. Wrap your app with <AdminProvider client={client}>");
13
+ return context;
14
+ }
15
+ /**
16
+ * Admin provider - provides CMS client and query client to the app
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * import { AdminProvider } from '@questpie/admin/hooks'
21
+ * import { createQuestpieClient } from 'questpie/client'
22
+ * import { QueryClient } from '@tanstack/react-query'
23
+ * import type { cms } from './server/cms'
24
+ *
25
+ * const client = createQuestpieClient<typeof cms>({
26
+ * baseURL: 'http://localhost:3000'
27
+ * })
28
+ *
29
+ * const queryClient = new QueryClient()
30
+ *
31
+ * function App() {
32
+ * return (
33
+ * <AdminProvider client={client} queryClient={queryClient}>
34
+ * <YourAdminApp />
35
+ * </AdminProvider>
36
+ * )
37
+ * }
38
+ * ```
39
+ */
40
+ function AdminProvider({ client, queryClient, locales, children }) {
41
+ const [defaultQueryClient] = React.useState(() => queryClient ?? new QueryClient());
42
+ const [locale, setLocale] = React.useState(() => {
43
+ if (locales?.default) return locales.default;
44
+ if (locales?.available?.length) return locales.available[0];
45
+ return typeof client.getLocale === "function" ? client.getLocale() : void 0;
46
+ });
47
+ React.useEffect(() => {
48
+ if (!locales?.available?.length) return;
49
+ if (!locale || !locales.available.includes(locale)) setLocale(locales.default ?? locales.available[0]);
50
+ }, [locale, locales]);
51
+ React.useEffect(() => {
52
+ if (typeof client.setLocale !== "function") return;
53
+ client.setLocale(locale);
54
+ }, [client, locale]);
55
+ return /* @__PURE__ */ jsx(AdminContextInstance.Provider, {
56
+ value: {
57
+ client,
58
+ locale,
59
+ setLocale,
60
+ locales
61
+ },
62
+ children: /* @__PURE__ */ jsx(QueryClientProvider, {
63
+ client: defaultQueryClient,
64
+ children
65
+ })
66
+ });
67
+ }
68
+
69
+ //#endregion
70
+ export { AdminProvider, useAdminContext };
@@ -0,0 +1,7 @@
1
+ import { AdminProvider, useAdminContext } from "./admin-provider";
2
+ import { createAdminAuthClient } from "./use-auth";
3
+ import { useCollection, useCollectionDelete, useCollectionInsert, useCollectionItemById, useCollectionUpdate } from "./use-collection-db";
4
+ import { useCollectionCreate, useCollectionDelete as useCollectionDeleteMutation, useCollectionItem, useCollectionList, useCollectionUpdate as useCollectionUpdateMutation } from "./use-collection";
5
+ import { useGlobal, useGlobalUpdate } from "./use-global";
6
+
7
+ export { AdminProvider, createAdminAuthClient, useAdminContext, useCollection, useCollectionCreate, useCollectionDelete, useCollectionDeleteMutation, useCollectionInsert, useCollectionItem, useCollectionItemById, useCollectionList, useCollectionUpdate, useCollectionUpdateMutation, useGlobal, useGlobalUpdate };
@@ -0,0 +1,178 @@
1
+ import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
2
+ import { atomWithStorage } from "jotai/utils";
3
+
4
+ //#region src/hooks/store.ts
5
+ /**
6
+ * Jotai Store for Admin State Management
7
+ *
8
+ * Centralized state atoms for the admin UI.
9
+ * Uses Jotai for reactive, atomic state management.
10
+ */
11
+ /**
12
+ * Sidebar collapsed state (persisted to localStorage)
13
+ */
14
+ const sidebarCollapsedAtom = atomWithStorage("admin:sidebar-collapsed", false);
15
+ /**
16
+ * Current active locale
17
+ */
18
+ const localeAtom = atom(null);
19
+ /**
20
+ * Available locales configuration
21
+ */
22
+ const localesConfigAtom = atom(null);
23
+ /**
24
+ * Mobile menu open state
25
+ */
26
+ const mobileMenuOpenAtom = atom(false);
27
+ /**
28
+ * Command palette open state
29
+ */
30
+ const commandPaletteOpenAtom = atom(false);
31
+ const themeModeAtom = atomWithStorage("admin:theme-mode", "system");
32
+ /**
33
+ * Derived atom for resolved theme (accounts for system preference)
34
+ */
35
+ const resolvedThemeAtom = atom((get) => {
36
+ const mode = get(themeModeAtom);
37
+ if (mode !== "system") return mode;
38
+ if (typeof window !== "undefined") return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
39
+ return "light";
40
+ });
41
+ /**
42
+ * Current active collection (for breadcrumbs, etc.)
43
+ */
44
+ const activeCollectionAtom = atom(null);
45
+ /**
46
+ * Current active item ID (for editing)
47
+ */
48
+ const activeItemIdAtom = atom(null);
49
+ /**
50
+ * Navigation history stack
51
+ */
52
+ const navigationHistoryAtom = atom([]);
53
+ /**
54
+ * Global form dirty state (to warn on navigation)
55
+ */
56
+ const formDirtyAtom = atom(false);
57
+ /**
58
+ * Active form errors count
59
+ */
60
+ const formErrorsCountAtom = atom(0);
61
+ const notificationsAtom = atom([]);
62
+ /**
63
+ * Derived atom to add notification
64
+ */
65
+ const addNotificationAtom = atom(null, (get, set, notification) => {
66
+ const id = Math.random().toString(36).substring(2, 9);
67
+ const newNotification = {
68
+ ...notification,
69
+ id
70
+ };
71
+ set(notificationsAtom, [...get(notificationsAtom), newNotification]);
72
+ if (notification.duration !== 0) setTimeout(() => {
73
+ set(notificationsAtom, (prev) => prev.filter((n) => n.id !== id));
74
+ }, notification.duration ?? 5e3);
75
+ return id;
76
+ });
77
+ /**
78
+ * Derived atom to remove notification
79
+ */
80
+ const removeNotificationAtom = atom(null, (get, set, id) => {
81
+ set(notificationsAtom, (prev) => prev.filter((n) => n.id !== id));
82
+ });
83
+ /**
84
+ * Hook for sidebar state
85
+ */
86
+ function useSidebar() {
87
+ const [collapsed, setCollapsed] = useAtom(sidebarCollapsedAtom);
88
+ return {
89
+ collapsed,
90
+ setCollapsed,
91
+ toggle: () => setCollapsed(!collapsed)
92
+ };
93
+ }
94
+ /**
95
+ * Hook for theme state
96
+ */
97
+ function useTheme() {
98
+ const [mode, setMode] = useAtom(themeModeAtom);
99
+ const resolved = useAtomValue(resolvedThemeAtom);
100
+ return {
101
+ mode,
102
+ setMode,
103
+ resolved,
104
+ isDark: resolved === "dark",
105
+ isLight: resolved === "light"
106
+ };
107
+ }
108
+ /**
109
+ * Hook for locale state
110
+ */
111
+ function useLocale() {
112
+ const [locale, setLocale] = useAtom(localeAtom);
113
+ const config = useAtomValue(localesConfigAtom);
114
+ return {
115
+ locale: locale ?? config?.default ?? null,
116
+ setLocale,
117
+ available: config?.available ?? [],
118
+ default: config?.default ?? null
119
+ };
120
+ }
121
+ /**
122
+ * Hook for notifications
123
+ */
124
+ function useNotifications() {
125
+ const notifications = useAtomValue(notificationsAtom);
126
+ const addNotification = useSetAtom(addNotificationAtom);
127
+ return {
128
+ notifications,
129
+ add: addNotification,
130
+ remove: useSetAtom(removeNotificationAtom),
131
+ success: (title, message) => addNotification({
132
+ type: "success",
133
+ title,
134
+ message
135
+ }),
136
+ error: (title, message) => addNotification({
137
+ type: "error",
138
+ title,
139
+ message
140
+ }),
141
+ warning: (title, message) => addNotification({
142
+ type: "warning",
143
+ title,
144
+ message
145
+ }),
146
+ info: (title, message) => addNotification({
147
+ type: "info",
148
+ title,
149
+ message
150
+ })
151
+ };
152
+ }
153
+ /**
154
+ * Hook for command palette
155
+ */
156
+ function useCommandPalette() {
157
+ const [open, setOpen] = useAtom(commandPaletteOpenAtom);
158
+ return {
159
+ open,
160
+ setOpen,
161
+ toggle: () => setOpen(!open)
162
+ };
163
+ }
164
+ /**
165
+ * Hook for form dirty state
166
+ */
167
+ function useFormDirty() {
168
+ const [dirty, setDirty] = useAtom(formDirtyAtom);
169
+ return {
170
+ dirty,
171
+ setDirty,
172
+ markDirty: () => setDirty(true),
173
+ markClean: () => setDirty(false)
174
+ };
175
+ }
176
+
177
+ //#endregion
178
+ export { activeCollectionAtom, activeItemIdAtom, addNotificationAtom, commandPaletteOpenAtom, formDirtyAtom, formErrorsCountAtom, localeAtom, localesConfigAtom, mobileMenuOpenAtom, navigationHistoryAtom, notificationsAtom, removeNotificationAtom, resolvedThemeAtom, sidebarCollapsedAtom, themeModeAtom, useCommandPalette, useFormDirty, useLocale, useNotifications, useSidebar, useTheme };
@@ -0,0 +1,76 @@
1
+ import { createAuthClient } from "better-auth/react";
2
+
3
+ //#region src/hooks/use-auth.ts
4
+ /**
5
+ * Auth client utilities for admin package
6
+ *
7
+ * Better Auth handles all auth state management internally.
8
+ * This module provides type-safe client creation helpers with
9
+ * full type inference from your CMS auth configuration.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * // In your app, create the auth client once:
14
+ * import { createAdminAuthClient } from '@questpie/admin/hooks'
15
+ * import type { cms } from './server/cms'
16
+ *
17
+ * export const authClient = createAdminAuthClient<typeof cms>({
18
+ * baseURL: 'http://localhost:3000'
19
+ * })
20
+ *
21
+ * // Then use the built-in hooks:
22
+ * function MyComponent() {
23
+ * const { data: session, isPending, error } = authClient.useSession()
24
+ *
25
+ * if (isPending) return <div>Loading...</div>
26
+ * if (!session) return <LoginForm />
27
+ *
28
+ * return <div>Welcome {session.user.name}</div>
29
+ * }
30
+ * ```
31
+ */
32
+ /**
33
+ * Create a type-safe Better Auth client for the admin UI
34
+ *
35
+ * The returned client includes full type inference from your CMS auth configuration:
36
+ * - `useSession()` - React hook for session state (typed based on your user/session schema)
37
+ * - `signIn` methods (email, social providers configured in CMS)
38
+ * - `signOut` - Sign out the current user
39
+ * - `signUp` - Register new users
40
+ * - Plugin-specific hooks (e.g., `useListAccounts` for admin plugin)
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * import { createAdminAuthClient } from '@questpie/admin/hooks'
45
+ * import type { cms } from './server/cms'
46
+ *
47
+ * // Create client with type inference from your CMS
48
+ * export const authClient = createAdminAuthClient<typeof cms>({
49
+ * baseURL: 'http://localhost:3000'
50
+ * })
51
+ *
52
+ * // Session data is fully typed based on your CMS auth config
53
+ * function App() {
54
+ * const { data: session, isPending } = authClient.useSession()
55
+ *
56
+ * if (isPending) return <Loading />
57
+ * if (!session) return <LoginPage authClient={authClient} />
58
+ *
59
+ * // session.user has all fields from your user schema
60
+ * return <AdminDashboard user={session.user} />
61
+ * }
62
+ * ```
63
+ */
64
+ function createAdminAuthClient(options) {
65
+ const basePath = options.basePath ?? "/cms/auth";
66
+ return createAuthClient({
67
+ baseURL: `${options.baseURL}${basePath}`,
68
+ fetchOptions: {
69
+ credentials: options.fetchOptions?.credentials ?? "include",
70
+ headers: options.fetchOptions?.headers
71
+ }
72
+ });
73
+ }
74
+
75
+ //#endregion
76
+ export { createAdminAuthClient };
@@ -0,0 +1,146 @@
1
+ import { useMemo } from "react";
2
+ import { useQueryClient } from "@tanstack/react-query";
3
+ import { useAdminContext } from "./admin-provider";
4
+ import { createQuestpieDBHelpers } from "@questpie/tanstack-query/db";
5
+
6
+ //#region src/hooks/use-collection-db.ts
7
+ /**
8
+ * Hook to get TanStack DB Collection for a CMS collection
9
+ * Provides offline-first, realtime-synced collection with optimistic updates
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * function PostsList() {
14
+ * const posts = useCollection('posts', {
15
+ * baseFindOptions: { where: { published: { eq: true } } },
16
+ * realtime: true, // Enable realtime sync
17
+ * })
18
+ *
19
+ * return (
20
+ * <div>
21
+ * {posts.items.map(post => (
22
+ * <div key={post.id}>{post.title}</div>
23
+ * ))}
24
+ * </div>
25
+ * )
26
+ * }
27
+ * ```
28
+ */
29
+ function useCollection(collectionName, options) {
30
+ const { client, locale } = useAdminContext();
31
+ const queryClient = useQueryClient();
32
+ const localeKey = locale ?? "default";
33
+ return useMemo(() => {
34
+ const collectionHelper = createQuestpieDBHelpers(client, { keyPrefix: [
35
+ "qcms",
36
+ "locale",
37
+ localeKey
38
+ ] }).collections[collectionName];
39
+ if (!collectionHelper) throw new Error(`Collection "${String(collectionName)}" not found in CMS`);
40
+ return collectionHelper.createCollection({
41
+ queryClient,
42
+ getKey: options?.getKey ?? ((item) => item.id),
43
+ baseFindOptions: options?.baseFindOptions,
44
+ realtime: options?.realtime ?? false
45
+ });
46
+ }, [
47
+ client,
48
+ queryClient,
49
+ collectionName,
50
+ localeKey,
51
+ options?.baseFindOptions,
52
+ options?.realtime,
53
+ options?.getKey
54
+ ]);
55
+ }
56
+ /**
57
+ * Hook to get a single item from collection by ID
58
+ * Uses TanStack DB Collection for offline-first access
59
+ *
60
+ * @example
61
+ * ```tsx
62
+ * function PostDetail({ id }: { id: string }) {
63
+ * const posts = useCollection('posts')
64
+ * const post = useCollectionItemById(posts, id)
65
+ *
66
+ * if (!post) return <div>Loading...</div>
67
+ *
68
+ * return <div>{post.title}</div>
69
+ * }
70
+ * ```
71
+ */
72
+ function useCollectionItemById(collection, id) {
73
+ return collection.toArray.find((item) => collection.config.getKey(item) === id);
74
+ }
75
+ /**
76
+ * Hook to create item in collection
77
+ * Uses TanStack DB Collection with optimistic updates
78
+ *
79
+ * @example
80
+ * ```tsx
81
+ * function CreatePost() {
82
+ * const posts = useCollection('posts')
83
+ *
84
+ * const handleCreate = async () => {
85
+ * await posts.insert({
86
+ * title: 'New Post',
87
+ * content: 'Hello World'
88
+ * })
89
+ * }
90
+ *
91
+ * return <button onClick={handleCreate}>Create Post</button>
92
+ * }
93
+ * ```
94
+ */
95
+ function useCollectionInsert(collectionName) {
96
+ const collection = useCollection(collectionName);
97
+ return collection.insert.bind(collection);
98
+ }
99
+ /**
100
+ * Hook to update item in collection
101
+ * Uses TanStack DB Collection with optimistic updates
102
+ *
103
+ * @example
104
+ * ```tsx
105
+ * function EditPost({ id }: { id: string }) {
106
+ * const posts = useCollection('posts')
107
+ * const post = useCollectionItemById(posts, id)
108
+ *
109
+ * const handleUpdate = async () => {
110
+ * await posts.update(id, {
111
+ * title: 'Updated Title'
112
+ * })
113
+ * }
114
+ *
115
+ * return <button onClick={handleUpdate}>Update</button>
116
+ * }
117
+ * ```
118
+ */
119
+ function useCollectionUpdate(collectionName) {
120
+ const collection = useCollection(collectionName);
121
+ return collection.update.bind(collection);
122
+ }
123
+ /**
124
+ * Hook to delete item from collection
125
+ * Uses TanStack DB Collection with optimistic updates
126
+ *
127
+ * @example
128
+ * ```tsx
129
+ * function DeletePost({ id }: { id: string }) {
130
+ * const posts = useCollection('posts')
131
+ *
132
+ * const handleDelete = async () => {
133
+ * await posts.delete(id)
134
+ * }
135
+ *
136
+ * return <button onClick={handleDelete}>Delete</button>
137
+ * }
138
+ * ```
139
+ */
140
+ function useCollectionDelete(collectionName) {
141
+ const collection = useCollection(collectionName);
142
+ return collection.delete.bind(collection);
143
+ }
144
+
145
+ //#endregion
146
+ export { useCollection, useCollectionDelete, useCollectionInsert, useCollectionItemById, useCollectionUpdate };