@btst/stack 2.8.1 → 2.9.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 (214) hide show
  1. package/README.md +3 -2
  2. package/dist/components/markdown/index.d.cts +15 -2
  3. package/dist/components/markdown/index.d.mts +15 -2
  4. package/dist/components/markdown/index.d.ts +15 -2
  5. package/dist/packages/stack/src/plugins/blog/client/components/forms/image-field.cjs +30 -1
  6. package/dist/packages/stack/src/plugins/blog/client/components/forms/image-field.mjs +30 -1
  7. package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.cjs +49 -9
  8. package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.mjs +50 -10
  9. package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.cjs +77 -9
  10. package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.mjs +77 -9
  11. package/dist/packages/stack/src/plugins/cms/client/components/forms/content-form.cjs +24 -5
  12. package/dist/packages/stack/src/plugins/cms/client/components/forms/content-form.mjs +24 -5
  13. package/dist/packages/stack/src/plugins/cms/client/components/forms/file-upload.cjs +47 -13
  14. package/dist/packages/stack/src/plugins/cms/client/components/forms/file-upload.mjs +47 -13
  15. package/dist/packages/stack/src/plugins/kanban/client/components/forms/board-form.cjs +1 -1
  16. package/dist/packages/stack/src/plugins/kanban/client/components/forms/board-form.mjs +1 -1
  17. package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.cjs +6 -2
  18. package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.mjs +6 -2
  19. package/dist/packages/stack/src/plugins/media/api/adapters/local.cjs +55 -0
  20. package/dist/packages/stack/src/plugins/media/api/adapters/local.mjs +37 -0
  21. package/dist/packages/stack/src/plugins/media/api/getters.cjs +83 -0
  22. package/dist/packages/stack/src/plugins/media/api/getters.mjs +78 -0
  23. package/dist/packages/stack/src/plugins/media/api/mutations.cjs +88 -0
  24. package/dist/packages/stack/src/plugins/media/api/mutations.mjs +82 -0
  25. package/dist/packages/stack/src/plugins/media/api/plugin.cjs +525 -0
  26. package/dist/packages/stack/src/plugins/media/api/plugin.mjs +523 -0
  27. package/dist/packages/stack/src/plugins/media/api/query-key-defs.cjs +19 -0
  28. package/dist/packages/stack/src/plugins/media/api/query-key-defs.mjs +16 -0
  29. package/dist/packages/stack/src/plugins/media/api/serializers.cjs +17 -0
  30. package/dist/packages/stack/src/plugins/media/api/serializers.mjs +14 -0
  31. package/dist/packages/stack/src/plugins/media/api/storage-adapter.cjs +15 -0
  32. package/dist/packages/stack/src/plugins/media/api/storage-adapter.mjs +11 -0
  33. package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-card.cjs +129 -0
  34. package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-card.mjs +127 -0
  35. package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-preview-button.cjs +58 -0
  36. package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-preview-button.mjs +56 -0
  37. package/dist/packages/stack/src/plugins/media/client/components/media-picker/browse-tab.cjs +94 -0
  38. package/dist/packages/stack/src/plugins/media/client/components/media-picker/browse-tab.mjs +92 -0
  39. package/dist/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.cjs +171 -0
  40. package/dist/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.mjs +168 -0
  41. package/dist/packages/stack/src/plugins/media/client/components/media-picker/index.cjs +308 -0
  42. package/dist/packages/stack/src/plugins/media/client/components/media-picker/index.mjs +305 -0
  43. package/dist/packages/stack/src/plugins/media/client/components/media-picker/upload-tab.cjs +104 -0
  44. package/dist/packages/stack/src/plugins/media/client/components/media-picker/upload-tab.mjs +102 -0
  45. package/dist/packages/stack/src/plugins/media/client/components/media-picker/url-tab.cjs +70 -0
  46. package/dist/packages/stack/src/plugins/media/client/components/media-picker/url-tab.mjs +68 -0
  47. package/dist/packages/stack/src/plugins/media/client/components/media-picker/utils.cjs +21 -0
  48. package/dist/packages/stack/src/plugins/media/client/components/media-picker/utils.mjs +17 -0
  49. package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.cjs +35 -0
  50. package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.internal.cjs +125 -0
  51. package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.internal.mjs +123 -0
  52. package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.mjs +33 -0
  53. package/dist/packages/stack/src/plugins/media/client/hooks/use-media.cjs +222 -0
  54. package/dist/packages/stack/src/plugins/media/client/hooks/use-media.mjs +214 -0
  55. package/dist/packages/stack/src/plugins/media/client/plugin.cjs +94 -0
  56. package/dist/packages/stack/src/plugins/media/client/plugin.mjs +92 -0
  57. package/dist/packages/stack/src/plugins/media/client/upload.cjs +121 -0
  58. package/dist/packages/stack/src/plugins/media/client/upload.mjs +119 -0
  59. package/dist/packages/stack/src/plugins/media/client/utils/image-compression.cjs +67 -0
  60. package/dist/packages/stack/src/plugins/media/client/utils/image-compression.mjs +65 -0
  61. package/dist/packages/stack/src/plugins/media/db.cjs +62 -0
  62. package/dist/packages/stack/src/plugins/media/db.mjs +60 -0
  63. package/dist/packages/stack/src/plugins/media/schemas.cjs +41 -0
  64. package/dist/packages/stack/src/plugins/media/schemas.mjs +35 -0
  65. package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-block.cjs +18 -1
  66. package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-block.mjs +19 -2
  67. package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-dialog.cjs +2 -2
  68. package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-dialog.mjs +2 -2
  69. package/dist/packages/ui/src/components/minimal-tiptap/components/section/five.cjs +3 -2
  70. package/dist/packages/ui/src/components/minimal-tiptap/components/section/five.mjs +3 -2
  71. package/dist/packages/ui/src/components/minimal-tiptap/minimal-tiptap.cjs +12 -5
  72. package/dist/packages/ui/src/components/minimal-tiptap/minimal-tiptap.mjs +12 -5
  73. package/dist/plugins/blog/api/index.d.cts +2 -2
  74. package/dist/plugins/blog/api/index.d.mts +2 -2
  75. package/dist/plugins/blog/api/index.d.ts +2 -2
  76. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  77. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  78. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  79. package/dist/plugins/blog/client/index.d.cts +60 -3
  80. package/dist/plugins/blog/client/index.d.mts +60 -3
  81. package/dist/plugins/blog/client/index.d.ts +60 -3
  82. package/dist/plugins/blog/query-keys.d.cts +2 -2
  83. package/dist/plugins/blog/query-keys.d.mts +2 -2
  84. package/dist/plugins/blog/query-keys.d.ts +2 -2
  85. package/dist/plugins/cms/client/index.d.cts +73 -3
  86. package/dist/plugins/cms/client/index.d.mts +73 -3
  87. package/dist/plugins/cms/client/index.d.ts +73 -3
  88. package/dist/plugins/kanban/api/index.d.cts +1 -1
  89. package/dist/plugins/kanban/api/index.d.mts +1 -1
  90. package/dist/plugins/kanban/api/index.d.ts +1 -1
  91. package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
  92. package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
  93. package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
  94. package/dist/plugins/kanban/client/index.d.cts +1 -1
  95. package/dist/plugins/kanban/client/index.d.mts +1 -1
  96. package/dist/plugins/kanban/client/index.d.ts +1 -1
  97. package/dist/plugins/kanban/query-keys.d.cts +1 -1
  98. package/dist/plugins/kanban/query-keys.d.mts +1 -1
  99. package/dist/plugins/kanban/query-keys.d.ts +1 -1
  100. package/dist/plugins/media/api/adapters/s3.cjs +106 -0
  101. package/dist/plugins/media/api/adapters/s3.d.cts +60 -0
  102. package/dist/plugins/media/api/adapters/s3.d.mts +60 -0
  103. package/dist/plugins/media/api/adapters/s3.d.ts +60 -0
  104. package/dist/plugins/media/api/adapters/s3.mjs +104 -0
  105. package/dist/plugins/media/api/adapters/vercel-blob.cjs +54 -0
  106. package/dist/plugins/media/api/adapters/vercel-blob.d.cts +41 -0
  107. package/dist/plugins/media/api/adapters/vercel-blob.d.mts +41 -0
  108. package/dist/plugins/media/api/adapters/vercel-blob.d.ts +41 -0
  109. package/dist/plugins/media/api/adapters/vercel-blob.mjs +52 -0
  110. package/dist/plugins/media/api/index.cjs +26 -0
  111. package/dist/plugins/media/api/index.d.cts +116 -0
  112. package/dist/plugins/media/api/index.d.mts +116 -0
  113. package/dist/plugins/media/api/index.d.ts +116 -0
  114. package/dist/plugins/media/api/index.mjs +6 -0
  115. package/dist/plugins/media/client/components/index.cjs +10 -0
  116. package/dist/plugins/media/client/components/index.d.cts +55 -0
  117. package/dist/plugins/media/client/components/index.d.mts +55 -0
  118. package/dist/plugins/media/client/components/index.d.ts +55 -0
  119. package/dist/plugins/media/client/components/index.mjs +2 -0
  120. package/dist/plugins/media/client/hooks/index.cjs +13 -0
  121. package/dist/plugins/media/client/hooks/index.d.cts +53 -0
  122. package/dist/plugins/media/client/hooks/index.d.mts +53 -0
  123. package/dist/plugins/media/client/hooks/index.d.ts +53 -0
  124. package/dist/plugins/media/client/hooks/index.mjs +1 -0
  125. package/dist/plugins/media/client/index.cjs +9 -0
  126. package/dist/plugins/media/client/index.d.cts +242 -0
  127. package/dist/plugins/media/client/index.d.mts +242 -0
  128. package/dist/plugins/media/client/index.d.ts +242 -0
  129. package/dist/plugins/media/client/index.mjs +2 -0
  130. package/dist/plugins/media/client.css +1 -0
  131. package/dist/plugins/media/query-keys.cjs +72 -0
  132. package/dist/plugins/media/query-keys.d.cts +49 -0
  133. package/dist/plugins/media/query-keys.d.mts +49 -0
  134. package/dist/plugins/media/query-keys.d.ts +49 -0
  135. package/dist/plugins/media/query-keys.mjs +70 -0
  136. package/dist/plugins/media/style.css +1 -0
  137. package/dist/shared/{stack.DOZ1EXjM.d.mts → stack.6mEHS2WH.d.mts} +3 -3
  138. package/dist/shared/{stack.DX-tQ93o.d.cts → stack.AJTXI7kw.d.cts} +3 -3
  139. package/dist/shared/{stack.DRpeDS6X.d.ts → stack.BMx2QYOK.d.ts} +25 -0
  140. package/dist/shared/stack.BUTXWiG-.d.ts +286 -0
  141. package/dist/shared/stack.C7Y9sBDg.d.mts +286 -0
  142. package/dist/shared/stack.C7vfOBmO.d.mts +63 -0
  143. package/dist/shared/stack.CAni8dnD.d.cts +63 -0
  144. package/dist/shared/stack.CLcnSF_b.d.cts +25 -0
  145. package/dist/shared/stack.CLcnSF_b.d.mts +25 -0
  146. package/dist/shared/stack.CLcnSF_b.d.ts +25 -0
  147. package/dist/shared/stack.CYSwntXC.d.ts +63 -0
  148. package/dist/shared/{stack.Jb0kQDJC.d.mts → stack.Cd6McBu1.d.mts} +25 -0
  149. package/dist/shared/stack.CoBj86jf.d.cts +109 -0
  150. package/dist/shared/stack.CoBj86jf.d.mts +109 -0
  151. package/dist/shared/stack.CoBj86jf.d.ts +109 -0
  152. package/dist/shared/{stack.BXxrFL9R.d.ts → stack.D7HSzZdG.d.ts} +5 -5
  153. package/dist/shared/{stack.DzOhpIYM.d.mts → stack.DjgpFWq3.d.cts} +5 -5
  154. package/dist/shared/{stack.BxFl46lB.d.cts → stack.DxQl8Wa1.d.cts} +25 -0
  155. package/dist/shared/{stack.BSqJrCTM.d.cts → stack.IUeyQKrm.d.mts} +5 -5
  156. package/dist/shared/{stack.VF6FhyZw.d.ts → stack.QYn-Px94.d.ts} +3 -3
  157. package/dist/shared/stack.vxskCkim.d.cts +286 -0
  158. package/package.json +113 -4
  159. package/src/plugins/blog/client/components/forms/image-field.tsx +35 -4
  160. package/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.tsx +67 -12
  161. package/src/plugins/blog/client/components/forms/markdown-editor.tsx +106 -10
  162. package/src/plugins/blog/client/overrides.ts +58 -1
  163. package/src/plugins/cms/client/components/forms/content-form.tsx +26 -7
  164. package/src/plugins/cms/client/components/forms/file-upload.tsx +73 -15
  165. package/src/plugins/cms/client/overrides.ts +57 -2
  166. package/src/plugins/kanban/client/components/forms/board-form.tsx +1 -1
  167. package/src/plugins/kanban/client/components/forms/task-form.tsx +7 -1
  168. package/src/plugins/kanban/client/overrides.ts +25 -0
  169. package/src/plugins/media/__tests__/__stubs__/vercel-blob-server.ts +9 -0
  170. package/src/plugins/media/__tests__/getters.test.ts +274 -0
  171. package/src/plugins/media/__tests__/mutations.test.ts +299 -0
  172. package/src/plugins/media/__tests__/plugin.test.ts +752 -0
  173. package/src/plugins/media/__tests__/query-key-defs.test.ts +54 -0
  174. package/src/plugins/media/__tests__/storage-adapters.test.ts +351 -0
  175. package/src/plugins/media/api/adapters/local.ts +79 -0
  176. package/src/plugins/media/api/adapters/s3.ts +198 -0
  177. package/src/plugins/media/api/adapters/vercel-blob.ts +132 -0
  178. package/src/plugins/media/api/getters.ts +174 -0
  179. package/src/plugins/media/api/index.ts +41 -0
  180. package/src/plugins/media/api/mutations.ts +179 -0
  181. package/src/plugins/media/api/plugin.ts +855 -0
  182. package/src/plugins/media/api/query-key-defs.ts +41 -0
  183. package/src/plugins/media/api/serializers.ts +28 -0
  184. package/src/plugins/media/api/storage-adapter.ts +139 -0
  185. package/src/plugins/media/client/components/index.tsx +6 -0
  186. package/src/plugins/media/client/components/media-picker/asset-card.tsx +150 -0
  187. package/src/plugins/media/client/components/media-picker/asset-preview-button.tsx +67 -0
  188. package/src/plugins/media/client/components/media-picker/browse-tab.tsx +116 -0
  189. package/src/plugins/media/client/components/media-picker/folder-tree.tsx +188 -0
  190. package/src/plugins/media/client/components/media-picker/index.tsx +347 -0
  191. package/src/plugins/media/client/components/media-picker/upload-tab.tsx +108 -0
  192. package/src/plugins/media/client/components/media-picker/url-tab.tsx +72 -0
  193. package/src/plugins/media/client/components/media-picker/utils.ts +17 -0
  194. package/src/plugins/media/client/components/pages/library-page.internal.tsx +134 -0
  195. package/src/plugins/media/client/components/pages/library-page.tsx +42 -0
  196. package/src/plugins/media/client/hooks/index.tsx +9 -0
  197. package/src/plugins/media/client/hooks/use-media.tsx +289 -0
  198. package/src/plugins/media/client/index.ts +4 -0
  199. package/src/plugins/media/client/overrides.ts +127 -0
  200. package/src/plugins/media/client/plugin.tsx +184 -0
  201. package/src/plugins/media/client/upload.ts +171 -0
  202. package/src/plugins/media/client/utils/image-compression.ts +131 -0
  203. package/src/plugins/media/client.css +1 -0
  204. package/src/plugins/media/db.ts +62 -0
  205. package/src/plugins/media/query-keys.ts +96 -0
  206. package/src/plugins/media/schemas.ts +37 -0
  207. package/src/plugins/media/style.css +1 -0
  208. package/src/plugins/media/types.ts +26 -0
  209. package/dist/shared/{stack.BWp0hcm9.d.ts → stack.BQmuNl5p.d.cts} +3 -3
  210. package/dist/shared/{stack.BWp0hcm9.d.cts → stack.BQmuNl5p.d.mts} +3 -3
  211. package/dist/shared/{stack.BWp0hcm9.d.mts → stack.BQmuNl5p.d.ts} +3 -3
  212. package/dist/shared/{stack.BvCR4-9H.d.ts → stack.D4Cea8II.d.ts} +3 -3
  213. package/dist/shared/{stack.CWxAl9K3.d.mts → stack.HE_IvqV5.d.mts} +3 -3
  214. package/dist/shared/{stack.BOokfhZD.d.cts → stack.Rtcvl8sS.d.cts} +3 -3
@@ -0,0 +1,214 @@
1
+ "use client";
2
+ import { useInfiniteQuery, useQuery, useQueryClient, useMutation } from '@tanstack/react-query';
3
+ import { usePluginOverrides } from '@btst/stack/context';
4
+ import { createApiClient } from '@btst/stack/plugins/client';
5
+ import { createMediaQueryKeys } from '../../../../../../../plugins/media/query-keys.mjs';
6
+ import { uploadAsset } from '../upload.mjs';
7
+
8
+ function useMediaConfig() {
9
+ return usePluginOverrides("media");
10
+ }
11
+ function useMediaApiClient() {
12
+ const { apiBaseURL, apiBasePath, headers } = useMediaConfig();
13
+ const client = createApiClient({
14
+ baseURL: apiBaseURL,
15
+ basePath: apiBasePath
16
+ });
17
+ return { client, headers };
18
+ }
19
+ function useAssets(params) {
20
+ const { client, headers } = useMediaApiClient();
21
+ const queries = createMediaQueryKeys(client, headers);
22
+ const { queryClient } = useMediaConfig();
23
+ params?.limit ?? 20;
24
+ return useInfiniteQuery(
25
+ {
26
+ ...queries.mediaAssets.list(params),
27
+ initialPageParam: 0,
28
+ refetchOnMount: "always",
29
+ getNextPageParam: (lastPage, _allPages, lastPageParam) => {
30
+ const offset = (lastPage.offset ?? 0) + lastPage.items.length;
31
+ return offset < lastPage.total ? offset : void 0;
32
+ }
33
+ },
34
+ queryClient
35
+ );
36
+ }
37
+ function useFolders(parentId) {
38
+ const { client, headers } = useMediaApiClient();
39
+ const queries = createMediaQueryKeys(client, headers);
40
+ const { queryClient } = useMediaConfig();
41
+ return useQuery(
42
+ {
43
+ ...queries.mediaFolders.list(parentId)
44
+ },
45
+ queryClient
46
+ );
47
+ }
48
+ function useUploadAsset() {
49
+ const {
50
+ apiBaseURL,
51
+ apiBasePath,
52
+ headers,
53
+ uploadMode = "direct",
54
+ imageCompression,
55
+ queryClient: qc
56
+ } = useMediaConfig();
57
+ const reactQueryClient = useQueryClient(qc);
58
+ return useMutation(
59
+ {
60
+ mutationFn: async ({
61
+ file,
62
+ folderId
63
+ }) => uploadAsset(
64
+ {
65
+ apiBaseURL,
66
+ apiBasePath,
67
+ headers,
68
+ uploadMode,
69
+ imageCompression
70
+ },
71
+ { file, folderId }
72
+ ),
73
+ onSuccess: () => {
74
+ reactQueryClient.invalidateQueries({ queryKey: ["mediaAssets"] });
75
+ }
76
+ },
77
+ qc
78
+ );
79
+ }
80
+ function useRegisterAsset() {
81
+ const {
82
+ apiBaseURL,
83
+ apiBasePath,
84
+ headers,
85
+ queryClient: qc
86
+ } = useMediaConfig();
87
+ const reactQueryClient = useQueryClient(qc);
88
+ return useMutation(
89
+ {
90
+ mutationFn: async (input) => {
91
+ const base = `${apiBaseURL}${apiBasePath}`;
92
+ const headersObj = new Headers(headers);
93
+ const res = await fetch(`${base}/media/assets`, {
94
+ method: "POST",
95
+ headers: {
96
+ ...Object.fromEntries(headersObj.entries()),
97
+ "Content-Type": "application/json"
98
+ },
99
+ body: JSON.stringify({
100
+ filename: input.filename,
101
+ originalName: input.filename,
102
+ mimeType: input.mimeType ?? "application/octet-stream",
103
+ size: input.size ?? 0,
104
+ url: input.url,
105
+ folderId: input.folderId
106
+ })
107
+ });
108
+ if (!res.ok) {
109
+ const err = await res.json().catch(() => ({ message: res.statusText }));
110
+ throw new Error(err.message ?? "Failed to register asset");
111
+ }
112
+ return res.json();
113
+ },
114
+ onSuccess: () => {
115
+ reactQueryClient.invalidateQueries({ queryKey: ["mediaAssets"] });
116
+ }
117
+ },
118
+ qc
119
+ );
120
+ }
121
+ function useDeleteAsset() {
122
+ const {
123
+ apiBaseURL,
124
+ apiBasePath,
125
+ headers,
126
+ queryClient: qc
127
+ } = useMediaConfig();
128
+ const reactQueryClient = useQueryClient(qc);
129
+ return useMutation(
130
+ {
131
+ mutationFn: async (id) => {
132
+ const base = `${apiBaseURL}${apiBasePath}`;
133
+ const headersObj = new Headers(headers);
134
+ const res = await fetch(`${base}/media/assets/${id}`, {
135
+ method: "DELETE",
136
+ headers: headersObj
137
+ });
138
+ if (!res.ok) {
139
+ const err = await res.json().catch(() => ({ message: res.statusText }));
140
+ throw new Error(err.message ?? "Delete failed");
141
+ }
142
+ },
143
+ onSuccess: () => {
144
+ reactQueryClient.invalidateQueries({ queryKey: ["mediaAssets"] });
145
+ }
146
+ },
147
+ qc
148
+ );
149
+ }
150
+ function useCreateFolder() {
151
+ const {
152
+ apiBaseURL,
153
+ apiBasePath,
154
+ headers,
155
+ queryClient: qc
156
+ } = useMediaConfig();
157
+ const reactQueryClient = useQueryClient(qc);
158
+ return useMutation(
159
+ {
160
+ mutationFn: async (input) => {
161
+ const base = `${apiBaseURL}${apiBasePath}`;
162
+ const headersObj = new Headers(headers);
163
+ const res = await fetch(`${base}/media/folders`, {
164
+ method: "POST",
165
+ headers: {
166
+ ...Object.fromEntries(headersObj.entries()),
167
+ "Content-Type": "application/json"
168
+ },
169
+ body: JSON.stringify(input)
170
+ });
171
+ if (!res.ok) {
172
+ const err = await res.json().catch(() => ({ message: res.statusText }));
173
+ throw new Error(err.message ?? "Failed to create folder");
174
+ }
175
+ return res.json();
176
+ },
177
+ onSuccess: () => {
178
+ reactQueryClient.invalidateQueries({ queryKey: ["mediaFolders"] });
179
+ }
180
+ },
181
+ qc
182
+ );
183
+ }
184
+ function useDeleteFolder() {
185
+ const {
186
+ apiBaseURL,
187
+ apiBasePath,
188
+ headers,
189
+ queryClient: qc
190
+ } = useMediaConfig();
191
+ const reactQueryClient = useQueryClient(qc);
192
+ return useMutation(
193
+ {
194
+ mutationFn: async (id) => {
195
+ const base = `${apiBaseURL}${apiBasePath}`;
196
+ const headersObj = new Headers(headers);
197
+ const res = await fetch(`${base}/media/folders/${id}`, {
198
+ method: "DELETE",
199
+ headers: headersObj
200
+ });
201
+ if (!res.ok) {
202
+ const err = await res.json().catch(() => ({ message: res.statusText }));
203
+ throw new Error(err.message ?? "Failed to delete folder");
204
+ }
205
+ },
206
+ onSuccess: () => {
207
+ reactQueryClient.invalidateQueries({ queryKey: ["mediaFolders"] });
208
+ }
209
+ },
210
+ qc
211
+ );
212
+ }
213
+
214
+ export { useAssets, useCreateFolder, useDeleteAsset, useDeleteFolder, useFolders, useRegisterAsset, useUploadAsset };
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ const client = require('@btst/stack/plugins/client');
4
+ const yar = require('@btst/yar');
5
+ const libraryPage = require('./components/pages/library-page.cjs');
6
+ const plugins_media_queryKeys = require('../../../../../../plugins/media/query-keys.cjs');
7
+
8
+ const mediaClientPlugin = (config) => client.defineClientPlugin({
9
+ name: "media",
10
+ routes: () => ({
11
+ library: yar.createRoute("/media", () => {
12
+ const CustomLibrary = config.pageComponents?.library;
13
+ return {
14
+ PageComponent: CustomLibrary ?? libraryPage.LibraryPageComponent,
15
+ loader: createMediaLibraryLoader(config),
16
+ meta: createMediaLibraryMeta(config)
17
+ };
18
+ })
19
+ })
20
+ });
21
+ function createMediaLibraryLoader(config) {
22
+ return async () => {
23
+ if (typeof window === "undefined") {
24
+ const { queryClient, apiBasePath, apiBaseURL, hooks, headers } = config;
25
+ const context = {
26
+ path: "/media",
27
+ isSSR: true,
28
+ apiBaseURL,
29
+ apiBasePath,
30
+ headers
31
+ };
32
+ try {
33
+ if (hooks?.beforeLoadLibrary) {
34
+ await hooks.beforeLoadLibrary(context);
35
+ }
36
+ const client$1 = client.createApiClient({
37
+ baseURL: apiBaseURL,
38
+ basePath: apiBasePath
39
+ });
40
+ const queries = plugins_media_queryKeys.createMediaQueryKeys(client$1, headers);
41
+ await queryClient.prefetchInfiniteQuery({
42
+ ...queries.mediaAssets.list({ limit: 40 }),
43
+ initialPageParam: 0
44
+ });
45
+ await queryClient.prefetchQuery(queries.mediaFolders.list(null));
46
+ if (hooks?.afterLoadLibrary) {
47
+ await hooks.afterLoadLibrary(context);
48
+ }
49
+ const queryState = queryClient.getQueryState(
50
+ queries.mediaAssets.list({ limit: 40 }).queryKey
51
+ );
52
+ if (queryState?.error && hooks?.onLoadError) {
53
+ const error = queryState.error instanceof Error ? queryState.error : new Error(String(queryState.error));
54
+ await hooks.onLoadError(error, context);
55
+ }
56
+ } catch (error) {
57
+ if (client.isConnectionError(error)) {
58
+ console.warn(
59
+ "[btst/media] route.loader() failed \u2014 no server running at build time. The media library does not support SSG."
60
+ );
61
+ }
62
+ if (hooks?.onLoadError) {
63
+ await hooks.onLoadError(error, context);
64
+ }
65
+ }
66
+ }
67
+ };
68
+ }
69
+ function createMediaLibraryMeta(config) {
70
+ return () => {
71
+ const { siteBaseURL, siteBasePath } = config;
72
+ const fullUrl = `${siteBaseURL}${siteBasePath}/media`;
73
+ const title = "Media Library";
74
+ return [
75
+ { title },
76
+ { name: "title", content: title },
77
+ { name: "description", content: "Manage your media assets" },
78
+ { name: "robots", content: "noindex, nofollow" },
79
+ // Open Graph
80
+ { property: "og:type", content: "website" },
81
+ { property: "og:title", content: title },
82
+ {
83
+ property: "og:description",
84
+ content: "Manage your media assets"
85
+ },
86
+ { property: "og:url", content: fullUrl },
87
+ // Twitter
88
+ { name: "twitter:card", content: "summary" },
89
+ { name: "twitter:title", content: title }
90
+ ];
91
+ };
92
+ }
93
+
94
+ exports.mediaClientPlugin = mediaClientPlugin;
@@ -0,0 +1,92 @@
1
+ import { defineClientPlugin, createApiClient, isConnectionError } from '@btst/stack/plugins/client';
2
+ import { createRoute } from '@btst/yar';
3
+ import { LibraryPageComponent } from './components/pages/library-page.mjs';
4
+ import { createMediaQueryKeys } from '../../../../../../plugins/media/query-keys.mjs';
5
+
6
+ const mediaClientPlugin = (config) => defineClientPlugin({
7
+ name: "media",
8
+ routes: () => ({
9
+ library: createRoute("/media", () => {
10
+ const CustomLibrary = config.pageComponents?.library;
11
+ return {
12
+ PageComponent: CustomLibrary ?? LibraryPageComponent,
13
+ loader: createMediaLibraryLoader(config),
14
+ meta: createMediaLibraryMeta(config)
15
+ };
16
+ })
17
+ })
18
+ });
19
+ function createMediaLibraryLoader(config) {
20
+ return async () => {
21
+ if (typeof window === "undefined") {
22
+ const { queryClient, apiBasePath, apiBaseURL, hooks, headers } = config;
23
+ const context = {
24
+ path: "/media",
25
+ isSSR: true,
26
+ apiBaseURL,
27
+ apiBasePath,
28
+ headers
29
+ };
30
+ try {
31
+ if (hooks?.beforeLoadLibrary) {
32
+ await hooks.beforeLoadLibrary(context);
33
+ }
34
+ const client = createApiClient({
35
+ baseURL: apiBaseURL,
36
+ basePath: apiBasePath
37
+ });
38
+ const queries = createMediaQueryKeys(client, headers);
39
+ await queryClient.prefetchInfiniteQuery({
40
+ ...queries.mediaAssets.list({ limit: 40 }),
41
+ initialPageParam: 0
42
+ });
43
+ await queryClient.prefetchQuery(queries.mediaFolders.list(null));
44
+ if (hooks?.afterLoadLibrary) {
45
+ await hooks.afterLoadLibrary(context);
46
+ }
47
+ const queryState = queryClient.getQueryState(
48
+ queries.mediaAssets.list({ limit: 40 }).queryKey
49
+ );
50
+ if (queryState?.error && hooks?.onLoadError) {
51
+ const error = queryState.error instanceof Error ? queryState.error : new Error(String(queryState.error));
52
+ await hooks.onLoadError(error, context);
53
+ }
54
+ } catch (error) {
55
+ if (isConnectionError(error)) {
56
+ console.warn(
57
+ "[btst/media] route.loader() failed \u2014 no server running at build time. The media library does not support SSG."
58
+ );
59
+ }
60
+ if (hooks?.onLoadError) {
61
+ await hooks.onLoadError(error, context);
62
+ }
63
+ }
64
+ }
65
+ };
66
+ }
67
+ function createMediaLibraryMeta(config) {
68
+ return () => {
69
+ const { siteBaseURL, siteBasePath } = config;
70
+ const fullUrl = `${siteBaseURL}${siteBasePath}/media`;
71
+ const title = "Media Library";
72
+ return [
73
+ { title },
74
+ { name: "title", content: title },
75
+ { name: "description", content: "Manage your media assets" },
76
+ { name: "robots", content: "noindex, nofollow" },
77
+ // Open Graph
78
+ { property: "og:type", content: "website" },
79
+ { property: "og:title", content: title },
80
+ {
81
+ property: "og:description",
82
+ content: "Manage your media assets"
83
+ },
84
+ { property: "og:url", content: fullUrl },
85
+ // Twitter
86
+ { name: "twitter:card", content: "summary" },
87
+ { name: "twitter:title", content: title }
88
+ ];
89
+ };
90
+ }
91
+
92
+ export { mediaClientPlugin };
@@ -0,0 +1,121 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ const imageCompression = require('./utils/image-compression.cjs');
5
+
6
+ const DEFAULT_IMAGE_COMPRESSION = {
7
+ maxWidth: 2048,
8
+ maxHeight: 2048,
9
+ quality: 0.85
10
+ };
11
+ async function uploadAsset(config, input) {
12
+ const {
13
+ apiBaseURL,
14
+ apiBasePath,
15
+ headers,
16
+ uploadMode = "direct",
17
+ imageCompression: imageCompression$1
18
+ } = config;
19
+ const { file, folderId } = input;
20
+ const processedFile = imageCompression$1 === false ? file : await imageCompression.compressImage(
21
+ file,
22
+ imageCompression$1 ?? DEFAULT_IMAGE_COMPRESSION
23
+ );
24
+ const base = `${apiBaseURL}${apiBasePath}`;
25
+ const headersObj = new Headers(headers);
26
+ if (uploadMode === "direct") {
27
+ const formData = new FormData();
28
+ formData.append("file", processedFile);
29
+ if (folderId) formData.append("folderId", folderId);
30
+ const res = await fetch(`${base}/media/upload`, {
31
+ method: "POST",
32
+ headers: headersObj,
33
+ body: formData
34
+ });
35
+ if (!res.ok) {
36
+ const err = await res.json().catch(() => ({ message: res.statusText }));
37
+ throw new Error(err.message ?? "Upload failed");
38
+ }
39
+ return res.json();
40
+ }
41
+ if (uploadMode === "s3") {
42
+ const tokenRes = await fetch(`${base}/media/upload/token`, {
43
+ method: "POST",
44
+ headers: {
45
+ ...Object.fromEntries(headersObj.entries()),
46
+ "Content-Type": "application/json"
47
+ },
48
+ body: JSON.stringify({
49
+ filename: processedFile.name,
50
+ mimeType: processedFile.type,
51
+ size: processedFile.size,
52
+ folderId
53
+ })
54
+ });
55
+ if (!tokenRes.ok) {
56
+ const err = await tokenRes.json().catch(() => ({ message: tokenRes.statusText }));
57
+ throw new Error(err.message ?? "Failed to get upload token");
58
+ }
59
+ const token = await tokenRes.json();
60
+ const putRes = await fetch(token.payload.uploadUrl, {
61
+ method: "PUT",
62
+ headers: token.payload.headers,
63
+ body: processedFile
64
+ });
65
+ if (!putRes.ok) throw new Error("Failed to upload to S3");
66
+ const assetRes = await fetch(`${base}/media/assets`, {
67
+ method: "POST",
68
+ headers: {
69
+ ...Object.fromEntries(headersObj.entries()),
70
+ "Content-Type": "application/json"
71
+ },
72
+ body: JSON.stringify({
73
+ filename: processedFile.name,
74
+ originalName: file.name,
75
+ mimeType: processedFile.type,
76
+ size: processedFile.size,
77
+ url: token.payload.publicUrl,
78
+ folderId
79
+ })
80
+ });
81
+ if (!assetRes.ok) {
82
+ const err = await assetRes.json().catch(() => ({ message: assetRes.statusText }));
83
+ throw new Error(err.message ?? "Failed to register asset");
84
+ }
85
+ return assetRes.json();
86
+ }
87
+ if (uploadMode === "vercel-blob") {
88
+ const { upload } = await import('@vercel/blob/client');
89
+ const blob = await upload(processedFile.name, processedFile, {
90
+ access: "public",
91
+ handleUploadUrl: `${base}/media/upload/vercel-blob`,
92
+ clientPayload: JSON.stringify({
93
+ mimeType: processedFile.type,
94
+ size: processedFile.size
95
+ })
96
+ });
97
+ const assetRes = await fetch(`${base}/media/assets`, {
98
+ method: "POST",
99
+ headers: {
100
+ ...Object.fromEntries(headersObj.entries()),
101
+ "Content-Type": "application/json"
102
+ },
103
+ body: JSON.stringify({
104
+ filename: processedFile.name,
105
+ originalName: file.name,
106
+ mimeType: processedFile.type,
107
+ size: processedFile.size,
108
+ url: blob.url,
109
+ folderId
110
+ })
111
+ });
112
+ if (!assetRes.ok) {
113
+ const err = await assetRes.json().catch(() => ({ message: assetRes.statusText }));
114
+ throw new Error(err.message ?? "Failed to register asset");
115
+ }
116
+ return assetRes.json();
117
+ }
118
+ throw new Error(`Unknown uploadMode: ${uploadMode}`);
119
+ }
120
+
121
+ exports.uploadAsset = uploadAsset;
@@ -0,0 +1,119 @@
1
+ "use client";
2
+ import { compressImage } from './utils/image-compression.mjs';
3
+
4
+ const DEFAULT_IMAGE_COMPRESSION = {
5
+ maxWidth: 2048,
6
+ maxHeight: 2048,
7
+ quality: 0.85
8
+ };
9
+ async function uploadAsset(config, input) {
10
+ const {
11
+ apiBaseURL,
12
+ apiBasePath,
13
+ headers,
14
+ uploadMode = "direct",
15
+ imageCompression
16
+ } = config;
17
+ const { file, folderId } = input;
18
+ const processedFile = imageCompression === false ? file : await compressImage(
19
+ file,
20
+ imageCompression ?? DEFAULT_IMAGE_COMPRESSION
21
+ );
22
+ const base = `${apiBaseURL}${apiBasePath}`;
23
+ const headersObj = new Headers(headers);
24
+ if (uploadMode === "direct") {
25
+ const formData = new FormData();
26
+ formData.append("file", processedFile);
27
+ if (folderId) formData.append("folderId", folderId);
28
+ const res = await fetch(`${base}/media/upload`, {
29
+ method: "POST",
30
+ headers: headersObj,
31
+ body: formData
32
+ });
33
+ if (!res.ok) {
34
+ const err = await res.json().catch(() => ({ message: res.statusText }));
35
+ throw new Error(err.message ?? "Upload failed");
36
+ }
37
+ return res.json();
38
+ }
39
+ if (uploadMode === "s3") {
40
+ const tokenRes = await fetch(`${base}/media/upload/token`, {
41
+ method: "POST",
42
+ headers: {
43
+ ...Object.fromEntries(headersObj.entries()),
44
+ "Content-Type": "application/json"
45
+ },
46
+ body: JSON.stringify({
47
+ filename: processedFile.name,
48
+ mimeType: processedFile.type,
49
+ size: processedFile.size,
50
+ folderId
51
+ })
52
+ });
53
+ if (!tokenRes.ok) {
54
+ const err = await tokenRes.json().catch(() => ({ message: tokenRes.statusText }));
55
+ throw new Error(err.message ?? "Failed to get upload token");
56
+ }
57
+ const token = await tokenRes.json();
58
+ const putRes = await fetch(token.payload.uploadUrl, {
59
+ method: "PUT",
60
+ headers: token.payload.headers,
61
+ body: processedFile
62
+ });
63
+ if (!putRes.ok) throw new Error("Failed to upload to S3");
64
+ const assetRes = await fetch(`${base}/media/assets`, {
65
+ method: "POST",
66
+ headers: {
67
+ ...Object.fromEntries(headersObj.entries()),
68
+ "Content-Type": "application/json"
69
+ },
70
+ body: JSON.stringify({
71
+ filename: processedFile.name,
72
+ originalName: file.name,
73
+ mimeType: processedFile.type,
74
+ size: processedFile.size,
75
+ url: token.payload.publicUrl,
76
+ folderId
77
+ })
78
+ });
79
+ if (!assetRes.ok) {
80
+ const err = await assetRes.json().catch(() => ({ message: assetRes.statusText }));
81
+ throw new Error(err.message ?? "Failed to register asset");
82
+ }
83
+ return assetRes.json();
84
+ }
85
+ if (uploadMode === "vercel-blob") {
86
+ const { upload } = await import('@vercel/blob/client');
87
+ const blob = await upload(processedFile.name, processedFile, {
88
+ access: "public",
89
+ handleUploadUrl: `${base}/media/upload/vercel-blob`,
90
+ clientPayload: JSON.stringify({
91
+ mimeType: processedFile.type,
92
+ size: processedFile.size
93
+ })
94
+ });
95
+ const assetRes = await fetch(`${base}/media/assets`, {
96
+ method: "POST",
97
+ headers: {
98
+ ...Object.fromEntries(headersObj.entries()),
99
+ "Content-Type": "application/json"
100
+ },
101
+ body: JSON.stringify({
102
+ filename: processedFile.name,
103
+ originalName: file.name,
104
+ mimeType: processedFile.type,
105
+ size: processedFile.size,
106
+ url: blob.url,
107
+ folderId
108
+ })
109
+ });
110
+ if (!assetRes.ok) {
111
+ const err = await assetRes.json().catch(() => ({ message: assetRes.statusText }));
112
+ throw new Error(err.message ?? "Failed to register asset");
113
+ }
114
+ return assetRes.json();
115
+ }
116
+ throw new Error(`Unknown uploadMode: ${uploadMode}`);
117
+ }
118
+
119
+ export { uploadAsset };
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ function loadImage(file) {
4
+ return new Promise((resolve, reject) => {
5
+ const url = URL.createObjectURL(file);
6
+ const img = new Image();
7
+ img.onload = () => {
8
+ URL.revokeObjectURL(url);
9
+ resolve(img);
10
+ };
11
+ img.onerror = () => {
12
+ URL.revokeObjectURL(url);
13
+ reject(new Error(`Failed to load image: ${file.name}`));
14
+ };
15
+ img.src = url;
16
+ });
17
+ }
18
+ const SKIP_TYPES = /* @__PURE__ */ new Set(["image/svg+xml", "image/gif"]);
19
+ async function compressImage(file, options = {}) {
20
+ if (!file.type.startsWith("image/") || SKIP_TYPES.has(file.type)) {
21
+ return file;
22
+ }
23
+ if (typeof document === "undefined") return file;
24
+ const {
25
+ maxWidth = 2048,
26
+ maxHeight = 2048,
27
+ quality = 0.85,
28
+ outputFormat
29
+ } = options;
30
+ const img = await loadImage(file);
31
+ let { width, height } = img;
32
+ const needsResize = width > maxWidth || height > maxHeight;
33
+ const needsFormatChange = outputFormat !== void 0 && outputFormat !== file.type;
34
+ if (!needsResize && !needsFormatChange) return file;
35
+ if (needsResize) {
36
+ const ratio = Math.min(maxWidth / width, maxHeight / height);
37
+ width = Math.round(width * ratio);
38
+ height = Math.round(height * ratio);
39
+ }
40
+ const canvas = document.createElement("canvas");
41
+ canvas.width = width;
42
+ canvas.height = height;
43
+ const ctx = canvas.getContext("2d");
44
+ if (!ctx) return file;
45
+ ctx.drawImage(img, 0, 0, width, height);
46
+ const mimeType = outputFormat ?? file.type;
47
+ return new Promise((resolve, reject) => {
48
+ canvas.toBlob(
49
+ (blob) => {
50
+ if (!blob) {
51
+ reject(new Error("canvas.toBlob returned null"));
52
+ return;
53
+ }
54
+ let name = file.name;
55
+ if (outputFormat && outputFormat !== file.type) {
56
+ const ext = outputFormat.split("/")[1] ?? "jpg";
57
+ name = name.replace(/\.[^.]+$/, `.${ext}`);
58
+ }
59
+ resolve(new File([blob], name, { type: mimeType }));
60
+ },
61
+ mimeType,
62
+ quality
63
+ );
64
+ });
65
+ }
66
+
67
+ exports.compressImage = compressImage;