@fnd-platform/cms 1.0.0-alpha.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 (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +283 -0
  3. package/lib/cms-project.d.ts +127 -0
  4. package/lib/cms-project.d.ts.map +1 -0
  5. package/lib/cms-project.js +343 -0
  6. package/lib/cms-project.js.map +1 -0
  7. package/lib/index.d.ts +11 -0
  8. package/lib/index.d.ts.map +1 -0
  9. package/lib/index.js +20 -0
  10. package/lib/index.js.map +1 -0
  11. package/lib/options.d.ts +59 -0
  12. package/lib/options.d.ts.map +1 -0
  13. package/lib/options.js +3 -0
  14. package/lib/options.js.map +1 -0
  15. package/lib/templates/admin-breadcrumbs.d.ts +13 -0
  16. package/lib/templates/admin-breadcrumbs.d.ts.map +1 -0
  17. package/lib/templates/admin-breadcrumbs.js +80 -0
  18. package/lib/templates/admin-breadcrumbs.js.map +1 -0
  19. package/lib/templates/admin-content-route.d.ts +18 -0
  20. package/lib/templates/admin-content-route.d.ts.map +1 -0
  21. package/lib/templates/admin-content-route.js +100 -0
  22. package/lib/templates/admin-content-route.js.map +1 -0
  23. package/lib/templates/admin-content-type-route.d.ts +9 -0
  24. package/lib/templates/admin-content-type-route.d.ts.map +1 -0
  25. package/lib/templates/admin-content-type-route.js +96 -0
  26. package/lib/templates/admin-content-type-route.js.map +1 -0
  27. package/lib/templates/admin-header.d.ts +13 -0
  28. package/lib/templates/admin-header.d.ts.map +1 -0
  29. package/lib/templates/admin-header.js +123 -0
  30. package/lib/templates/admin-header.js.map +1 -0
  31. package/lib/templates/admin-index.d.ts +9 -0
  32. package/lib/templates/admin-index.d.ts.map +1 -0
  33. package/lib/templates/admin-index.js +60 -0
  34. package/lib/templates/admin-index.js.map +1 -0
  35. package/lib/templates/admin-layout.d.ts +10 -0
  36. package/lib/templates/admin-layout.d.ts.map +1 -0
  37. package/lib/templates/admin-layout.js +46 -0
  38. package/lib/templates/admin-layout.js.map +1 -0
  39. package/lib/templates/admin-sidebar.d.ts +13 -0
  40. package/lib/templates/admin-sidebar.d.ts.map +1 -0
  41. package/lib/templates/admin-sidebar.js +149 -0
  42. package/lib/templates/admin-sidebar.js.map +1 -0
  43. package/lib/templates/content-editor.d.ts +10 -0
  44. package/lib/templates/content-editor.d.ts.map +1 -0
  45. package/lib/templates/content-editor.js +354 -0
  46. package/lib/templates/content-editor.js.map +1 -0
  47. package/lib/templates/content-schema.d.ts +10 -0
  48. package/lib/templates/content-schema.d.ts.map +1 -0
  49. package/lib/templates/content-schema.js +274 -0
  50. package/lib/templates/content-schema.js.map +1 -0
  51. package/lib/templates/content-table.d.ts +13 -0
  52. package/lib/templates/content-table.d.ts.map +1 -0
  53. package/lib/templates/content-table.js +177 -0
  54. package/lib/templates/content-table.js.map +1 -0
  55. package/lib/templates/content-types-examples.d.ts +19 -0
  56. package/lib/templates/content-types-examples.d.ts.map +1 -0
  57. package/lib/templates/content-types-examples.js +275 -0
  58. package/lib/templates/content-types-examples.js.map +1 -0
  59. package/lib/templates/content-types-registry.d.ts +10 -0
  60. package/lib/templates/content-types-registry.d.ts.map +1 -0
  61. package/lib/templates/content-types-registry.js +87 -0
  62. package/lib/templates/content-types-registry.js.map +1 -0
  63. package/lib/templates/content-types.d.ts +10 -0
  64. package/lib/templates/content-types.d.ts.map +1 -0
  65. package/lib/templates/content-types.js +384 -0
  66. package/lib/templates/content-types.js.map +1 -0
  67. package/lib/templates/dashboard-stats.d.ts +13 -0
  68. package/lib/templates/dashboard-stats.d.ts.map +1 -0
  69. package/lib/templates/dashboard-stats.js +117 -0
  70. package/lib/templates/dashboard-stats.js.map +1 -0
  71. package/lib/templates/editor/index.d.ts +6 -0
  72. package/lib/templates/editor/index.d.ts.map +1 -0
  73. package/lib/templates/editor/index.js +21 -0
  74. package/lib/templates/editor/index.js.map +1 -0
  75. package/lib/templates/editor/rich-text-editor.d.ts +7 -0
  76. package/lib/templates/editor/rich-text-editor.d.ts.map +1 -0
  77. package/lib/templates/editor/rich-text-editor.js +115 -0
  78. package/lib/templates/editor/rich-text-editor.js.map +1 -0
  79. package/lib/templates/editor/toolbar.d.ts +7 -0
  80. package/lib/templates/editor/toolbar.d.ts.map +1 -0
  81. package/lib/templates/editor/toolbar.js +272 -0
  82. package/lib/templates/editor/toolbar.js.map +1 -0
  83. package/lib/templates/form-fields/boolean-field.d.ts +7 -0
  84. package/lib/templates/form-fields/boolean-field.d.ts.map +1 -0
  85. package/lib/templates/form-fields/boolean-field.js +76 -0
  86. package/lib/templates/form-fields/boolean-field.js.map +1 -0
  87. package/lib/templates/form-fields/date-field.d.ts +7 -0
  88. package/lib/templates/form-fields/date-field.d.ts.map +1 -0
  89. package/lib/templates/form-fields/date-field.js +61 -0
  90. package/lib/templates/form-fields/date-field.js.map +1 -0
  91. package/lib/templates/form-fields/datetime-field.d.ts +7 -0
  92. package/lib/templates/form-fields/datetime-field.d.ts.map +1 -0
  93. package/lib/templates/form-fields/datetime-field.js +87 -0
  94. package/lib/templates/form-fields/datetime-field.js.map +1 -0
  95. package/lib/templates/form-fields/index.d.ts +23 -0
  96. package/lib/templates/form-fields/index.d.ts.map +1 -0
  97. package/lib/templates/form-fields/index.js +275 -0
  98. package/lib/templates/form-fields/index.js.map +1 -0
  99. package/lib/templates/form-fields/media-field.d.ts +10 -0
  100. package/lib/templates/form-fields/media-field.d.ts.map +1 -0
  101. package/lib/templates/form-fields/media-field.js +225 -0
  102. package/lib/templates/form-fields/media-field.js.map +1 -0
  103. package/lib/templates/form-fields/multiselect-field.d.ts +7 -0
  104. package/lib/templates/form-fields/multiselect-field.d.ts.map +1 -0
  105. package/lib/templates/form-fields/multiselect-field.js +121 -0
  106. package/lib/templates/form-fields/multiselect-field.js.map +1 -0
  107. package/lib/templates/form-fields/number-field.d.ts +7 -0
  108. package/lib/templates/form-fields/number-field.d.ts.map +1 -0
  109. package/lib/templates/form-fields/number-field.js +87 -0
  110. package/lib/templates/form-fields/number-field.js.map +1 -0
  111. package/lib/templates/form-fields/reference-field.d.ts +9 -0
  112. package/lib/templates/form-fields/reference-field.d.ts.map +1 -0
  113. package/lib/templates/form-fields/reference-field.js +145 -0
  114. package/lib/templates/form-fields/reference-field.js.map +1 -0
  115. package/lib/templates/form-fields/richtext-field.d.ts +9 -0
  116. package/lib/templates/form-fields/richtext-field.d.ts.map +1 -0
  117. package/lib/templates/form-fields/richtext-field.js +60 -0
  118. package/lib/templates/form-fields/richtext-field.js.map +1 -0
  119. package/lib/templates/form-fields/select-field.d.ts +7 -0
  120. package/lib/templates/form-fields/select-field.d.ts.map +1 -0
  121. package/lib/templates/form-fields/select-field.js +70 -0
  122. package/lib/templates/form-fields/select-field.js.map +1 -0
  123. package/lib/templates/form-fields/slug-field.d.ts +7 -0
  124. package/lib/templates/form-fields/slug-field.d.ts.map +1 -0
  125. package/lib/templates/form-fields/slug-field.js +143 -0
  126. package/lib/templates/form-fields/slug-field.js.map +1 -0
  127. package/lib/templates/form-fields/tags-field.d.ts +7 -0
  128. package/lib/templates/form-fields/tags-field.d.ts.map +1 -0
  129. package/lib/templates/form-fields/tags-field.js +172 -0
  130. package/lib/templates/form-fields/tags-field.js.map +1 -0
  131. package/lib/templates/form-fields/text-field.d.ts +7 -0
  132. package/lib/templates/form-fields/text-field.d.ts.map +1 -0
  133. package/lib/templates/form-fields/text-field.js +63 -0
  134. package/lib/templates/form-fields/text-field.js.map +1 -0
  135. package/lib/templates/form-fields/textarea-field.d.ts +7 -0
  136. package/lib/templates/form-fields/textarea-field.d.ts.map +1 -0
  137. package/lib/templates/form-fields/textarea-field.js +64 -0
  138. package/lib/templates/form-fields/textarea-field.js.map +1 -0
  139. package/lib/templates/index.d.ts +34 -0
  140. package/lib/templates/index.d.ts.map +1 -0
  141. package/lib/templates/index.js +92 -0
  142. package/lib/templates/index.js.map +1 -0
  143. package/lib/templates/media/index.d.ts +12 -0
  144. package/lib/templates/media/index.d.ts.map +1 -0
  145. package/lib/templates/media/index.js +50 -0
  146. package/lib/templates/media/index.js.map +1 -0
  147. package/lib/templates/media/media-api.d.ts +13 -0
  148. package/lib/templates/media/media-api.d.ts.map +1 -0
  149. package/lib/templates/media/media-api.js +274 -0
  150. package/lib/templates/media/media-api.js.map +1 -0
  151. package/lib/templates/media/media-grid.d.ts +14 -0
  152. package/lib/templates/media/media-grid.d.ts.map +1 -0
  153. package/lib/templates/media/media-grid.js +314 -0
  154. package/lib/templates/media/media-grid.js.map +1 -0
  155. package/lib/templates/media/media-library-route.d.ts +13 -0
  156. package/lib/templates/media/media-library-route.d.ts.map +1 -0
  157. package/lib/templates/media/media-library-route.js +105 -0
  158. package/lib/templates/media/media-library-route.js.map +1 -0
  159. package/lib/templates/media/media-picker.d.ts +13 -0
  160. package/lib/templates/media/media-picker.d.ts.map +1 -0
  161. package/lib/templates/media/media-picker.js +152 -0
  162. package/lib/templates/media/media-picker.js.map +1 -0
  163. package/lib/templates/media/media-uploader.d.ts +14 -0
  164. package/lib/templates/media/media-uploader.d.ts.map +1 -0
  165. package/lib/templates/media/media-uploader.js +318 -0
  166. package/lib/templates/media/media-uploader.js.map +1 -0
  167. package/lib/templates/recent-content.d.ts +13 -0
  168. package/lib/templates/recent-content.d.ts.map +1 -0
  169. package/lib/templates/recent-content.js +138 -0
  170. package/lib/templates/recent-content.js.map +1 -0
  171. package/lib/templates/slug-utils.d.ts +10 -0
  172. package/lib/templates/slug-utils.d.ts.map +1 -0
  173. package/lib/templates/slug-utils.js +194 -0
  174. package/lib/templates/slug-utils.js.map +1 -0
  175. package/lib/templates/ui-avatar.d.ts +8 -0
  176. package/lib/templates/ui-avatar.d.ts.map +1 -0
  177. package/lib/templates/ui-avatar.js +60 -0
  178. package/lib/templates/ui-avatar.js.map +1 -0
  179. package/lib/templates/ui-badge.d.ts +8 -0
  180. package/lib/templates/ui-badge.d.ts.map +1 -0
  181. package/lib/templates/ui-badge.js +52 -0
  182. package/lib/templates/ui-badge.js.map +1 -0
  183. package/lib/templates/ui-dialog.d.ts +10 -0
  184. package/lib/templates/ui-dialog.d.ts.map +1 -0
  185. package/lib/templates/ui-dialog.js +134 -0
  186. package/lib/templates/ui-dialog.js.map +1 -0
  187. package/lib/templates/ui-dropdown-menu.d.ts +8 -0
  188. package/lib/templates/ui-dropdown-menu.d.ts.map +1 -0
  189. package/lib/templates/ui-dropdown-menu.js +210 -0
  190. package/lib/templates/ui-dropdown-menu.js.map +1 -0
  191. package/lib/templates/ui-popover.d.ts +8 -0
  192. package/lib/templates/ui-popover.d.ts.map +1 -0
  193. package/lib/templates/ui-popover.js +43 -0
  194. package/lib/templates/ui-popover.js.map +1 -0
  195. package/lib/templates/ui-progress.d.ts +10 -0
  196. package/lib/templates/ui-progress.d.ts.map +1 -0
  197. package/lib/templates/ui-progress.js +40 -0
  198. package/lib/templates/ui-progress.js.map +1 -0
  199. package/lib/templates/ui-table.d.ts +8 -0
  200. package/lib/templates/ui-table.d.ts.map +1 -0
  201. package/lib/templates/ui-table.js +129 -0
  202. package/lib/templates/ui-table.js.map +1 -0
  203. package/lib/templates/ui-tabs.d.ts +10 -0
  204. package/lib/templates/ui-tabs.d.ts.map +1 -0
  205. package/lib/templates/ui-tabs.js +67 -0
  206. package/lib/templates/ui-tabs.js.map +1 -0
  207. package/package.json +52 -0
@@ -0,0 +1,274 @@
1
+ 'use strict';
2
+ Object.defineProperty(exports, '__esModule', { value: true });
3
+ exports.getMediaApiTemplate = getMediaApiTemplate;
4
+ /**
5
+ * Generates the media API client template.
6
+ *
7
+ * This template provides client-side helpers for:
8
+ * - Requesting presigned URLs
9
+ * - Uploading files directly to S3
10
+ * - Creating media records
11
+ * - Listing and deleting media
12
+ *
13
+ * @returns Template string for app/lib/media-api.ts
14
+ */
15
+ function getMediaApiTemplate() {
16
+ return `/**
17
+ * Media API client for presigned URL uploads.
18
+ *
19
+ * This module handles the upload flow:
20
+ * 1. Request presigned URL from API
21
+ * 2. Upload file directly to S3
22
+ * 3. Create media record in API
23
+ */
24
+
25
+ export interface MediaItem {
26
+ id: string;
27
+ key: string;
28
+ filename: string;
29
+ mimeType: string;
30
+ size: number;
31
+ url: string;
32
+ uploadedAt: string;
33
+ }
34
+
35
+ export interface UploadProgress {
36
+ loaded: number;
37
+ total: number;
38
+ percentage: number;
39
+ }
40
+
41
+ export interface UploadOptions {
42
+ onProgress?: (progress: UploadProgress) => void;
43
+ signal?: AbortSignal;
44
+ }
45
+
46
+ /**
47
+ * Allowed media MIME types.
48
+ */
49
+ export const ALLOWED_MIME_TYPES = [
50
+ 'image/jpeg',
51
+ 'image/png',
52
+ 'image/gif',
53
+ 'image/webp',
54
+ 'image/svg+xml',
55
+ 'application/pdf',
56
+ 'video/mp4',
57
+ 'video/webm',
58
+ 'audio/mpeg',
59
+ 'audio/wav',
60
+ 'audio/ogg',
61
+ ] as const;
62
+
63
+ export type AllowedMimeType = typeof ALLOWED_MIME_TYPES[number];
64
+
65
+ /**
66
+ * Validates if a MIME type is allowed.
67
+ */
68
+ export function isAllowedMimeType(mimeType: string): mimeType is AllowedMimeType {
69
+ return ALLOWED_MIME_TYPES.includes(mimeType as AllowedMimeType);
70
+ }
71
+
72
+ /**
73
+ * Gets the media type category from a MIME type.
74
+ */
75
+ export function getMediaType(mimeType: string): 'image' | 'video' | 'audio' | 'document' | 'unknown' {
76
+ if (mimeType.startsWith('image/')) return 'image';
77
+ if (mimeType.startsWith('video/')) return 'video';
78
+ if (mimeType.startsWith('audio/')) return 'audio';
79
+ if (mimeType === 'application/pdf') return 'document';
80
+ return 'unknown';
81
+ }
82
+
83
+ /**
84
+ * Request a presigned URL for file upload.
85
+ */
86
+ export async function getUploadUrl(file: File): Promise<{
87
+ uploadUrl: string;
88
+ key: string;
89
+ id: string;
90
+ expiresIn: number;
91
+ }> {
92
+ const response = await fetch('/api/media/upload-url', {
93
+ method: 'POST',
94
+ headers: {
95
+ 'Content-Type': 'application/json',
96
+ },
97
+ body: JSON.stringify({
98
+ filename: file.name,
99
+ contentType: file.type,
100
+ size: file.size,
101
+ }),
102
+ });
103
+
104
+ if (!response.ok) {
105
+ const error = await response.json();
106
+ throw new Error(error.error?.message || 'Failed to get upload URL');
107
+ }
108
+
109
+ const result = await response.json();
110
+ return result.data;
111
+ }
112
+
113
+ /**
114
+ * Upload a file directly to S3 using a presigned URL.
115
+ * Uses XMLHttpRequest for progress tracking.
116
+ */
117
+ export function uploadToS3(
118
+ file: File,
119
+ uploadUrl: string,
120
+ options: UploadOptions = {}
121
+ ): Promise<void> {
122
+ return new Promise((resolve, reject) => {
123
+ const xhr = new XMLHttpRequest();
124
+
125
+ // Handle abort signal
126
+ if (options.signal) {
127
+ options.signal.addEventListener('abort', () => {
128
+ xhr.abort();
129
+ reject(new DOMException('Upload aborted', 'AbortError'));
130
+ });
131
+ }
132
+
133
+ // Progress tracking
134
+ xhr.upload.onprogress = (event) => {
135
+ if (event.lengthComputable && options.onProgress) {
136
+ options.onProgress({
137
+ loaded: event.loaded,
138
+ total: event.total,
139
+ percentage: Math.round((event.loaded / event.total) * 100),
140
+ });
141
+ }
142
+ };
143
+
144
+ xhr.onload = () => {
145
+ if (xhr.status >= 200 && xhr.status < 300) {
146
+ resolve();
147
+ } else {
148
+ reject(new Error(\`Upload failed with status \${xhr.status}\`));
149
+ }
150
+ };
151
+
152
+ xhr.onerror = () => {
153
+ reject(new Error('Upload failed'));
154
+ };
155
+
156
+ xhr.open('PUT', uploadUrl);
157
+ xhr.setRequestHeader('Content-Type', file.type);
158
+ xhr.send(file);
159
+ });
160
+ }
161
+
162
+ /**
163
+ * Create a media record after successful S3 upload.
164
+ */
165
+ export async function createMediaRecord(params: {
166
+ id: string;
167
+ key: string;
168
+ filename: string;
169
+ mimeType: string;
170
+ size: number;
171
+ }): Promise<MediaItem> {
172
+ const response = await fetch('/api/media', {
173
+ method: 'POST',
174
+ headers: {
175
+ 'Content-Type': 'application/json',
176
+ },
177
+ body: JSON.stringify(params),
178
+ });
179
+
180
+ if (!response.ok) {
181
+ const error = await response.json();
182
+ throw new Error(error.error?.message || 'Failed to create media record');
183
+ }
184
+
185
+ const result = await response.json();
186
+ return result.data;
187
+ }
188
+
189
+ /**
190
+ * Upload a file with the complete presigned URL flow.
191
+ */
192
+ export async function uploadFile(
193
+ file: File,
194
+ options: UploadOptions = {}
195
+ ): Promise<MediaItem> {
196
+ // Validate file type
197
+ if (!isAllowedMimeType(file.type)) {
198
+ throw new Error(\`File type \${file.type} is not allowed\`);
199
+ }
200
+
201
+ // Step 1: Get presigned URL
202
+ const { uploadUrl, key, id } = await getUploadUrl(file);
203
+
204
+ // Step 2: Upload to S3
205
+ await uploadToS3(file, uploadUrl, options);
206
+
207
+ // Step 3: Create media record
208
+ const media = await createMediaRecord({
209
+ id,
210
+ key,
211
+ filename: file.name,
212
+ mimeType: file.type,
213
+ size: file.size,
214
+ });
215
+
216
+ return media;
217
+ }
218
+
219
+ /**
220
+ * List media items with optional filtering.
221
+ */
222
+ export async function listMedia(params: {
223
+ limit?: number;
224
+ cursor?: string;
225
+ type?: 'image' | 'video' | 'audio' | 'document';
226
+ } = {}): Promise<{
227
+ items: MediaItem[];
228
+ nextCursor?: string;
229
+ total?: number;
230
+ }> {
231
+ const searchParams = new URLSearchParams();
232
+ if (params.limit) searchParams.set('limit', params.limit.toString());
233
+ if (params.cursor) searchParams.set('cursor', params.cursor);
234
+ if (params.type) searchParams.set('type', params.type);
235
+
236
+ const url = \`/api/media?\${searchParams.toString()}\`;
237
+ const response = await fetch(url);
238
+
239
+ if (!response.ok) {
240
+ const error = await response.json();
241
+ throw new Error(error.error?.message || 'Failed to list media');
242
+ }
243
+
244
+ const result = await response.json();
245
+ return result.data;
246
+ }
247
+
248
+ /**
249
+ * Delete a media item.
250
+ */
251
+ export async function deleteMedia(id: string): Promise<void> {
252
+ const response = await fetch(\`/api/media/\${id}\`, {
253
+ method: 'DELETE',
254
+ });
255
+
256
+ if (!response.ok) {
257
+ const error = await response.json();
258
+ throw new Error(error.error?.message || 'Failed to delete media');
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Format file size for display.
264
+ */
265
+ export function formatFileSize(bytes: number): string {
266
+ if (bytes === 0) return '0 Bytes';
267
+ const k = 1024;
268
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
269
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
270
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
271
+ }
272
+ `;
273
+ }
274
+ //# sourceMappingURL=media-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-api.js","sourceRoot":"","sources":["../../../src/templates/media/media-api.ts"],"names":[],"mappings":";;AAWA,kDAkQC;AA7QD;;;;;;;;;;GAUG;AACH,SAAgB,mBAAmB;IACjC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgQR,CAAC;AACF,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Generates the media grid component template.
3
+ *
4
+ * This component provides:
5
+ * - Grid display of media items
6
+ * - Selection support (single/multiple)
7
+ * - Type filtering
8
+ * - Pagination
9
+ * - Delete functionality
10
+ *
11
+ * @returns Template string for app/components/media/media-grid.tsx
12
+ */
13
+ export declare function getMediaGridTemplate(): string;
14
+ //# sourceMappingURL=media-grid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-grid.d.ts","sourceRoot":"","sources":["../../../src/templates/media/media-grid.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAyS7C"}
@@ -0,0 +1,314 @@
1
+ 'use strict';
2
+ Object.defineProperty(exports, '__esModule', { value: true });
3
+ exports.getMediaGridTemplate = getMediaGridTemplate;
4
+ /**
5
+ * Generates the media grid component template.
6
+ *
7
+ * This component provides:
8
+ * - Grid display of media items
9
+ * - Selection support (single/multiple)
10
+ * - Type filtering
11
+ * - Pagination
12
+ * - Delete functionality
13
+ *
14
+ * @returns Template string for app/components/media/media-grid.tsx
15
+ */
16
+ function getMediaGridTemplate() {
17
+ return `import { useState, useEffect, useCallback } from 'react';
18
+ import {
19
+ Image,
20
+ FileVideo,
21
+ FileAudio,
22
+ File,
23
+ Trash2,
24
+ Check,
25
+ Loader2,
26
+ ChevronLeft,
27
+ ChevronRight,
28
+ } from 'lucide-react';
29
+ import { cn } from '~/lib/utils';
30
+ import { Button } from '~/components/ui/button';
31
+ import {
32
+ Select,
33
+ SelectContent,
34
+ SelectItem,
35
+ SelectTrigger,
36
+ SelectValue,
37
+ } from '~/components/ui/select';
38
+ import {
39
+ listMedia,
40
+ deleteMedia,
41
+ getMediaType,
42
+ formatFileSize,
43
+ type MediaItem,
44
+ } from '~/lib/media-api';
45
+
46
+ interface MediaGridProps {
47
+ onSelect?: (media: MediaItem) => void;
48
+ onSelectionChange?: (media: MediaItem[]) => void;
49
+ selectedIds?: string[];
50
+ multiple?: boolean;
51
+ accept?: string[];
52
+ className?: string;
53
+ }
54
+
55
+ /**
56
+ * Get icon for file type.
57
+ */
58
+ function getMediaIcon(mimeType: string) {
59
+ const type = getMediaType(mimeType);
60
+ switch (type) {
61
+ case 'image':
62
+ return <Image className="h-8 w-8 text-muted-foreground" />;
63
+ case 'video':
64
+ return <FileVideo className="h-8 w-8 text-muted-foreground" />;
65
+ case 'audio':
66
+ return <FileAudio className="h-8 w-8 text-muted-foreground" />;
67
+ default:
68
+ return <File className="h-8 w-8 text-muted-foreground" />;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Check if media item is an image.
74
+ */
75
+ function isImage(mimeType: string): boolean {
76
+ return mimeType.startsWith('image/') && mimeType !== 'image/svg+xml';
77
+ }
78
+
79
+ export function MediaGrid({
80
+ onSelect,
81
+ onSelectionChange,
82
+ selectedIds = [],
83
+ multiple = false,
84
+ accept,
85
+ className,
86
+ }: MediaGridProps) {
87
+ const [media, setMedia] = useState<MediaItem[]>([]);
88
+ const [loading, setLoading] = useState(true);
89
+ const [error, setError] = useState<string | null>(null);
90
+ const [typeFilter, setTypeFilter] = useState<string>('all');
91
+ const [cursor, setCursor] = useState<string | undefined>();
92
+ const [hasMore, setHasMore] = useState(false);
93
+ const [deleting, setDeleting] = useState<string | null>(null);
94
+
95
+ const loadMedia = useCallback(async (reset = false) => {
96
+ setLoading(true);
97
+ setError(null);
98
+ try {
99
+ const type = typeFilter !== 'all' ? typeFilter as 'image' | 'video' | 'audio' | 'document' : undefined;
100
+ const result = await listMedia({
101
+ limit: 20,
102
+ cursor: reset ? undefined : cursor,
103
+ type,
104
+ });
105
+
106
+ if (reset) {
107
+ setMedia(result.items);
108
+ } else {
109
+ setMedia((prev) => [...prev, ...result.items]);
110
+ }
111
+ setCursor(result.nextCursor);
112
+ setHasMore(!!result.nextCursor);
113
+ } catch (err) {
114
+ setError((err as Error).message);
115
+ } finally {
116
+ setLoading(false);
117
+ }
118
+ }, [typeFilter, cursor]);
119
+
120
+ useEffect(() => {
121
+ loadMedia(true);
122
+ }, [typeFilter]);
123
+
124
+ const handleSelect = (item: MediaItem) => {
125
+ // Check if item type is accepted
126
+ if (accept && accept.length > 0) {
127
+ const isAccepted = accept.some((acceptType) => {
128
+ if (acceptType.endsWith('/*')) {
129
+ return item.mimeType.startsWith(acceptType.replace('/*', '/'));
130
+ }
131
+ return item.mimeType === acceptType;
132
+ });
133
+ if (!isAccepted) return;
134
+ }
135
+
136
+ if (multiple) {
137
+ const isSelected = selectedIds.includes(item.id);
138
+ const newSelection = isSelected
139
+ ? selectedIds.filter((id) => id !== item.id)
140
+ : [...selectedIds, item.id];
141
+
142
+ const selectedItems = media.filter((m) => newSelection.includes(m.id));
143
+ onSelectionChange?.(selectedItems);
144
+ } else {
145
+ onSelect?.(item);
146
+ }
147
+ };
148
+
149
+ const handleDelete = async (item: MediaItem, e: React.MouseEvent) => {
150
+ e.stopPropagation();
151
+ if (!confirm(\`Delete "\${item.filename}"?\`)) return;
152
+
153
+ setDeleting(item.id);
154
+ try {
155
+ await deleteMedia(item.id);
156
+ setMedia((prev) => prev.filter((m) => m.id !== item.id));
157
+ } catch (err) {
158
+ setError((err as Error).message);
159
+ } finally {
160
+ setDeleting(null);
161
+ }
162
+ };
163
+
164
+ const isSelected = (id: string) => selectedIds.includes(id);
165
+
166
+ const isAccepted = (mimeType: string) => {
167
+ if (!accept || accept.length === 0) return true;
168
+ return accept.some((acceptType) => {
169
+ if (acceptType.endsWith('/*')) {
170
+ return mimeType.startsWith(acceptType.replace('/*', '/'));
171
+ }
172
+ return mimeType === acceptType;
173
+ });
174
+ };
175
+
176
+ return (
177
+ <div className={cn('space-y-4', className)}>
178
+ {/* Filters */}
179
+ <div className="flex items-center gap-4">
180
+ <Select value={typeFilter} onValueChange={(v) => setTypeFilter(v)}>
181
+ <SelectTrigger className="w-[180px]">
182
+ <SelectValue placeholder="Filter by type" />
183
+ </SelectTrigger>
184
+ <SelectContent>
185
+ <SelectItem value="all">All Types</SelectItem>
186
+ <SelectItem value="image">Images</SelectItem>
187
+ <SelectItem value="video">Videos</SelectItem>
188
+ <SelectItem value="audio">Audio</SelectItem>
189
+ <SelectItem value="document">Documents</SelectItem>
190
+ </SelectContent>
191
+ </Select>
192
+
193
+ {selectedIds.length > 0 && (
194
+ <span className="text-sm text-muted-foreground">
195
+ {selectedIds.length} selected
196
+ </span>
197
+ )}
198
+ </div>
199
+
200
+ {/* Error message */}
201
+ {error && (
202
+ <div className="rounded-lg bg-destructive/10 p-4 text-sm text-destructive">
203
+ {error}
204
+ </div>
205
+ )}
206
+
207
+ {/* Loading state */}
208
+ {loading && media.length === 0 && (
209
+ <div className="flex items-center justify-center py-12">
210
+ <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
211
+ </div>
212
+ )}
213
+
214
+ {/* Empty state */}
215
+ {!loading && media.length === 0 && (
216
+ <div className="flex flex-col items-center justify-center py-12 text-center">
217
+ <Image className="h-12 w-12 text-muted-foreground/50" />
218
+ <p className="mt-4 text-sm text-muted-foreground">
219
+ No media files found
220
+ </p>
221
+ </div>
222
+ )}
223
+
224
+ {/* Grid */}
225
+ {media.length > 0 && (
226
+ <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
227
+ {media.map((item) => {
228
+ const accepted = isAccepted(item.mimeType);
229
+ const selected = isSelected(item.id);
230
+
231
+ return (
232
+ <div
233
+ key={item.id}
234
+ onClick={() => accepted && handleSelect(item)}
235
+ className={cn(
236
+ 'group relative aspect-square cursor-pointer overflow-hidden rounded-lg border bg-muted transition-all',
237
+ selected && 'ring-2 ring-primary ring-offset-2',
238
+ !accepted && 'cursor-not-allowed opacity-50',
239
+ accepted && 'hover:border-primary/50'
240
+ )}
241
+ >
242
+ {/* Preview */}
243
+ {isImage(item.mimeType) ? (
244
+ <img
245
+ src={item.url}
246
+ alt={item.filename}
247
+ className="h-full w-full object-cover"
248
+ />
249
+ ) : (
250
+ <div className="flex h-full w-full items-center justify-center">
251
+ {getMediaIcon(item.mimeType)}
252
+ </div>
253
+ )}
254
+
255
+ {/* Selection indicator */}
256
+ {selected && (
257
+ <div className="absolute left-2 top-2 flex h-6 w-6 items-center justify-center rounded-full bg-primary text-primary-foreground">
258
+ <Check className="h-4 w-4" />
259
+ </div>
260
+ )}
261
+
262
+ {/* Hover overlay */}
263
+ <div className="absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-black/60 to-transparent p-2 opacity-0 transition-opacity group-hover:opacity-100">
264
+ <p className="truncate text-xs font-medium text-white">
265
+ {item.filename}
266
+ </p>
267
+ <p className="text-xs text-white/70">
268
+ {formatFileSize(item.size)}
269
+ </p>
270
+ </div>
271
+
272
+ {/* Delete button */}
273
+ <button
274
+ onClick={(e) => handleDelete(item, e)}
275
+ className="absolute right-2 top-2 flex h-7 w-7 items-center justify-center rounded-full bg-destructive text-destructive-foreground opacity-0 transition-opacity hover:bg-destructive/90 group-hover:opacity-100"
276
+ disabled={deleting === item.id}
277
+ >
278
+ {deleting === item.id ? (
279
+ <Loader2 className="h-4 w-4 animate-spin" />
280
+ ) : (
281
+ <Trash2 className="h-4 w-4" />
282
+ )}
283
+ </button>
284
+ </div>
285
+ );
286
+ })}
287
+ </div>
288
+ )}
289
+
290
+ {/* Load more */}
291
+ {hasMore && (
292
+ <div className="flex justify-center pt-4">
293
+ <Button
294
+ variant="outline"
295
+ onClick={() => loadMedia(false)}
296
+ disabled={loading}
297
+ >
298
+ {loading ? (
299
+ <>
300
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
301
+ Loading...
302
+ </>
303
+ ) : (
304
+ 'Load More'
305
+ )}
306
+ </Button>
307
+ </div>
308
+ )}
309
+ </div>
310
+ );
311
+ }
312
+ `;
313
+ }
314
+ //# sourceMappingURL=media-grid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-grid.js","sourceRoot":"","sources":["../../../src/templates/media/media-grid.ts"],"names":[],"mappings":";;AAYA,oDAySC;AArTD;;;;;;;;;;;GAWG;AACH,SAAgB,oBAAoB;IAClC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuSR,CAAC;AACF,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Generates the media library route template.
3
+ *
4
+ * This is the main media library page in the admin interface.
5
+ * It provides:
6
+ * - Full media upload functionality
7
+ * - Grid view of all media
8
+ * - Filtering and pagination
9
+ *
10
+ * @returns Template string for app/routes/_admin.media.tsx
11
+ */
12
+ export declare function getMediaLibraryRouteTemplate(): string;
13
+ //# sourceMappingURL=media-library-route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-library-route.d.ts","sourceRoot":"","sources":["../../../src/templates/media/media-library-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,wBAAgB,4BAA4B,IAAI,MAAM,CAyFrD"}