@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,105 @@
1
+ 'use strict';
2
+ Object.defineProperty(exports, '__esModule', { value: true });
3
+ exports.getMediaLibraryRouteTemplate = getMediaLibraryRouteTemplate;
4
+ /**
5
+ * Generates the media library route template.
6
+ *
7
+ * This is the main media library page in the admin interface.
8
+ * It provides:
9
+ * - Full media upload functionality
10
+ * - Grid view of all media
11
+ * - Filtering and pagination
12
+ *
13
+ * @returns Template string for app/routes/_admin.media.tsx
14
+ */
15
+ function getMediaLibraryRouteTemplate() {
16
+ return `import type { MetaFunction, LoaderFunctionArgs } from '@remix-run/node';
17
+ import { json } from '@remix-run/node';
18
+ import { useLoaderData, useOutletContext } from '@remix-run/react';
19
+ import { requireAuth, hasAnyRole } from '~/lib/auth.server';
20
+ import { Breadcrumbs } from '~/components/admin/breadcrumbs';
21
+ import { MediaUploader } from '~/components/media/media-uploader';
22
+ import { MediaGrid } from '~/components/media/media-grid';
23
+ import type { MediaItem } from '~/lib/media-api';
24
+ import { useState, useCallback } from 'react';
25
+
26
+ export const meta: MetaFunction = () => {
27
+ return [
28
+ { title: 'Media Library - CMS Admin' },
29
+ { name: 'description', content: 'Manage media files in the CMS' },
30
+ ];
31
+ };
32
+
33
+ export async function loader({ request }: LoaderFunctionArgs) {
34
+ const user = await requireAuth(request);
35
+
36
+ // Check if user has admin or editor role
37
+ if (!hasAnyRole(user, ['admin', 'editor'])) {
38
+ throw new Response('Forbidden', { status: 403 });
39
+ }
40
+
41
+ return json({ user });
42
+ }
43
+
44
+ interface AdminContext {
45
+ user: { email: string };
46
+ isAdmin: boolean;
47
+ isEditor: boolean;
48
+ }
49
+
50
+ export default function MediaLibraryPage() {
51
+ const { user } = useLoaderData<typeof loader>();
52
+ const context = useOutletContext<AdminContext>();
53
+ const [refreshKey, setRefreshKey] = useState(0);
54
+
55
+ const handleUploadComplete = useCallback((media: MediaItem) => {
56
+ // Refresh the grid by updating the key
57
+ setRefreshKey((k) => k + 1);
58
+ }, []);
59
+
60
+ const handleSelect = (media: MediaItem) => {
61
+ // Could open a detail modal or copy URL to clipboard
62
+ navigator.clipboard?.writeText(media.url);
63
+ };
64
+
65
+ return (
66
+ <div className="space-y-6">
67
+ <Breadcrumbs
68
+ items={[
69
+ { label: 'Admin', href: '/admin' },
70
+ { label: 'Media Library' },
71
+ ]}
72
+ />
73
+
74
+ <div className="flex items-center justify-between">
75
+ <div>
76
+ <h1 className="text-2xl font-semibold">Media Library</h1>
77
+ <p className="text-sm text-muted-foreground">
78
+ Upload and manage media files
79
+ </p>
80
+ </div>
81
+ </div>
82
+
83
+ {/* Upload Section */}
84
+ <div className="rounded-lg border bg-card p-6">
85
+ <h2 className="mb-4 text-lg font-medium">Upload Files</h2>
86
+ <MediaUploader
87
+ onUploadComplete={handleUploadComplete}
88
+ multiple
89
+ />
90
+ </div>
91
+
92
+ {/* Media Grid */}
93
+ <div className="rounded-lg border bg-card p-6">
94
+ <h2 className="mb-4 text-lg font-medium">All Media</h2>
95
+ <MediaGrid
96
+ key={refreshKey}
97
+ onSelect={handleSelect}
98
+ />
99
+ </div>
100
+ </div>
101
+ );
102
+ }
103
+ `;
104
+ }
105
+ //# sourceMappingURL=media-library-route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-library-route.js","sourceRoot":"","sources":["../../../src/templates/media/media-library-route.ts"],"names":[],"mappings":";;AAWA,oEAyFC;AApGD;;;;;;;;;;GAUG;AACH,SAAgB,4BAA4B;IAC1C,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuFR,CAAC;AACF,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Generates the media picker dialog component template.
3
+ *
4
+ * This component provides:
5
+ * - Dialog for selecting media
6
+ * - Upload and browse tabs
7
+ * - Single/multiple selection
8
+ * - Type filtering based on field config
9
+ *
10
+ * @returns Template string for app/components/media/media-picker.tsx
11
+ */
12
+ export declare function getMediaPickerTemplate(): string;
13
+ //# sourceMappingURL=media-picker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-picker.d.ts","sourceRoot":"","sources":["../../../src/templates/media/media-picker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAwI/C"}
@@ -0,0 +1,152 @@
1
+ 'use strict';
2
+ Object.defineProperty(exports, '__esModule', { value: true });
3
+ exports.getMediaPickerTemplate = getMediaPickerTemplate;
4
+ /**
5
+ * Generates the media picker dialog component template.
6
+ *
7
+ * This component provides:
8
+ * - Dialog for selecting media
9
+ * - Upload and browse tabs
10
+ * - Single/multiple selection
11
+ * - Type filtering based on field config
12
+ *
13
+ * @returns Template string for app/components/media/media-picker.tsx
14
+ */
15
+ function getMediaPickerTemplate() {
16
+ return `import { useState, useCallback } from 'react';
17
+ import { Upload, FolderOpen } from 'lucide-react';
18
+ import {
19
+ Dialog,
20
+ DialogContent,
21
+ DialogHeader,
22
+ DialogTitle,
23
+ DialogFooter,
24
+ } from '~/components/ui/dialog';
25
+ import { Button } from '~/components/ui/button';
26
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '~/components/ui/tabs';
27
+ import { MediaUploader } from './media-uploader';
28
+ import { MediaGrid } from './media-grid';
29
+ import type { MediaItem } from '~/lib/media-api';
30
+
31
+ interface MediaPickerProps {
32
+ open: boolean;
33
+ onOpenChange: (open: boolean) => void;
34
+ onSelect: (media: MediaItem | MediaItem[]) => void;
35
+ value?: string | string[];
36
+ multiple?: boolean;
37
+ accept?: string[];
38
+ title?: string;
39
+ }
40
+
41
+ export function MediaPicker({
42
+ open,
43
+ onOpenChange,
44
+ onSelect,
45
+ value,
46
+ multiple = false,
47
+ accept,
48
+ title = 'Select Media',
49
+ }: MediaPickerProps) {
50
+ const [activeTab, setActiveTab] = useState<'browse' | 'upload'>('browse');
51
+ const [selectedIds, setSelectedIds] = useState<string[]>(() => {
52
+ if (!value) return [];
53
+ return Array.isArray(value) ? value : [value];
54
+ });
55
+ const [selectedMedia, setSelectedMedia] = useState<MediaItem[]>([]);
56
+
57
+ const handleUploadComplete = useCallback((media: MediaItem) => {
58
+ if (multiple) {
59
+ setSelectedMedia((prev) => [...prev, media]);
60
+ setSelectedIds((prev) => [...prev, media.id]);
61
+ } else {
62
+ // For single selection, select and close
63
+ onSelect(media);
64
+ onOpenChange(false);
65
+ }
66
+ }, [multiple, onSelect, onOpenChange]);
67
+
68
+ const handleSelect = useCallback((media: MediaItem) => {
69
+ // For single selection from grid
70
+ onSelect(media);
71
+ onOpenChange(false);
72
+ }, [onSelect, onOpenChange]);
73
+
74
+ const handleSelectionChange = useCallback((media: MediaItem[]) => {
75
+ setSelectedMedia(media);
76
+ setSelectedIds(media.map((m) => m.id));
77
+ }, []);
78
+
79
+ const handleConfirm = () => {
80
+ if (multiple) {
81
+ onSelect(selectedMedia);
82
+ }
83
+ onOpenChange(false);
84
+ };
85
+
86
+ const handleCancel = () => {
87
+ setSelectedIds(Array.isArray(value) ? value : value ? [value] : []);
88
+ setSelectedMedia([]);
89
+ onOpenChange(false);
90
+ };
91
+
92
+ return (
93
+ <Dialog open={open} onOpenChange={onOpenChange}>
94
+ <DialogContent className="max-w-4xl max-h-[80vh] flex flex-col">
95
+ <DialogHeader>
96
+ <DialogTitle>{title}</DialogTitle>
97
+ </DialogHeader>
98
+
99
+ <Tabs
100
+ value={activeTab}
101
+ onValueChange={(v) => setActiveTab(v as 'browse' | 'upload')}
102
+ className="flex-1 flex flex-col min-h-0"
103
+ >
104
+ <TabsList className="grid w-full grid-cols-2">
105
+ <TabsTrigger value="browse" className="flex items-center gap-2">
106
+ <FolderOpen className="h-4 w-4" />
107
+ Browse Library
108
+ </TabsTrigger>
109
+ <TabsTrigger value="upload" className="flex items-center gap-2">
110
+ <Upload className="h-4 w-4" />
111
+ Upload New
112
+ </TabsTrigger>
113
+ </TabsList>
114
+
115
+ <div className="flex-1 overflow-y-auto min-h-0 py-4">
116
+ <TabsContent value="browse" className="mt-0 h-full">
117
+ <MediaGrid
118
+ onSelect={multiple ? undefined : handleSelect}
119
+ onSelectionChange={multiple ? handleSelectionChange : undefined}
120
+ selectedIds={selectedIds}
121
+ multiple={multiple}
122
+ accept={accept}
123
+ />
124
+ </TabsContent>
125
+
126
+ <TabsContent value="upload" className="mt-0">
127
+ <MediaUploader
128
+ onUploadComplete={handleUploadComplete}
129
+ accept={accept}
130
+ multiple={multiple}
131
+ />
132
+ </TabsContent>
133
+ </div>
134
+ </Tabs>
135
+
136
+ <DialogFooter>
137
+ <Button variant="outline" onClick={handleCancel}>
138
+ Cancel
139
+ </Button>
140
+ {multiple && (
141
+ <Button onClick={handleConfirm} disabled={selectedIds.length === 0}>
142
+ Select {selectedIds.length > 0 ? \`(\${selectedIds.length})\` : ''}
143
+ </Button>
144
+ )}
145
+ </DialogFooter>
146
+ </DialogContent>
147
+ </Dialog>
148
+ );
149
+ }
150
+ `;
151
+ }
152
+ //# sourceMappingURL=media-picker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-picker.js","sourceRoot":"","sources":["../../../src/templates/media/media-picker.ts"],"names":[],"mappings":";;AAWA,wDAwIC;AAnJD;;;;;;;;;;GAUG;AACH,SAAgB,sBAAsB;IACpC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsIR,CAAC;AACF,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Generates the media uploader component template.
3
+ *
4
+ * This component provides:
5
+ * - Drag and drop file upload
6
+ * - Click to select files
7
+ * - Upload progress tracking
8
+ * - Multiple file support
9
+ * - File type validation
10
+ *
11
+ * @returns Template string for app/components/media/media-uploader.tsx
12
+ */
13
+ export declare function getMediaUploaderTemplate(): string;
14
+ //# sourceMappingURL=media-uploader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-uploader.d.ts","sourceRoot":"","sources":["../../../src/templates/media/media-uploader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CA6SjD"}
@@ -0,0 +1,318 @@
1
+ 'use strict';
2
+ Object.defineProperty(exports, '__esModule', { value: true });
3
+ exports.getMediaUploaderTemplate = getMediaUploaderTemplate;
4
+ /**
5
+ * Generates the media uploader component template.
6
+ *
7
+ * This component provides:
8
+ * - Drag and drop file upload
9
+ * - Click to select files
10
+ * - Upload progress tracking
11
+ * - Multiple file support
12
+ * - File type validation
13
+ *
14
+ * @returns Template string for app/components/media/media-uploader.tsx
15
+ */
16
+ function getMediaUploaderTemplate() {
17
+ return `import { useCallback, useState, useRef } from 'react';
18
+ import { Upload, X, FileImage, FileVideo, FileAudio, File } from 'lucide-react';
19
+ import { cn } from '~/lib/utils';
20
+ import { Button } from '~/components/ui/button';
21
+ import { Progress } from '~/components/ui/progress';
22
+ import {
23
+ uploadFile,
24
+ isAllowedMimeType,
25
+ getMediaType,
26
+ formatFileSize,
27
+ type MediaItem,
28
+ type UploadProgress,
29
+ } from '~/lib/media-api';
30
+
31
+ interface UploadingFile {
32
+ id: string;
33
+ file: File;
34
+ progress: number;
35
+ status: 'uploading' | 'complete' | 'error';
36
+ error?: string;
37
+ abortController: AbortController;
38
+ }
39
+
40
+ interface MediaUploaderProps {
41
+ onUploadComplete?: (media: MediaItem) => void;
42
+ onUploadError?: (error: Error, file: File) => void;
43
+ accept?: string[];
44
+ maxSize?: number;
45
+ multiple?: boolean;
46
+ className?: string;
47
+ }
48
+
49
+ /**
50
+ * Get icon for file type.
51
+ */
52
+ function getFileIcon(mimeType: string) {
53
+ const type = getMediaType(mimeType);
54
+ switch (type) {
55
+ case 'image':
56
+ return <FileImage className="h-8 w-8 text-blue-500" />;
57
+ case 'video':
58
+ return <FileVideo className="h-8 w-8 text-purple-500" />;
59
+ case 'audio':
60
+ return <FileAudio className="h-8 w-8 text-green-500" />;
61
+ default:
62
+ return <File className="h-8 w-8 text-gray-500" />;
63
+ }
64
+ }
65
+
66
+ export function MediaUploader({
67
+ onUploadComplete,
68
+ onUploadError,
69
+ accept,
70
+ maxSize = 52428800, // 50MB default
71
+ multiple = true,
72
+ className,
73
+ }: MediaUploaderProps) {
74
+ const [isDragging, setIsDragging] = useState(false);
75
+ const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>([]);
76
+ const inputRef = useRef<HTMLInputElement>(null);
77
+
78
+ const handleFiles = useCallback(
79
+ async (files: FileList | File[]) => {
80
+ const fileArray = Array.from(files);
81
+
82
+ for (const file of fileArray) {
83
+ // Validate file type
84
+ if (!isAllowedMimeType(file.type)) {
85
+ onUploadError?.(new Error(\`File type \${file.type} is not allowed\`), file);
86
+ continue;
87
+ }
88
+
89
+ // Validate file size
90
+ if (file.size > maxSize) {
91
+ onUploadError?.(
92
+ new Error(\`File size exceeds maximum of \${formatFileSize(maxSize)}\`),
93
+ file
94
+ );
95
+ continue;
96
+ }
97
+
98
+ // Check accept filter
99
+ if (accept && accept.length > 0) {
100
+ const isAccepted = accept.some((acceptType) => {
101
+ if (acceptType.endsWith('/*')) {
102
+ return file.type.startsWith(acceptType.replace('/*', '/'));
103
+ }
104
+ return file.type === acceptType;
105
+ });
106
+ if (!isAccepted) {
107
+ onUploadError?.(new Error(\`File type \${file.type} is not accepted\`), file);
108
+ continue;
109
+ }
110
+ }
111
+
112
+ // Create upload entry
113
+ const uploadId = \`\${Date.now()}-\${Math.random().toString(36).substr(2, 9)}\`;
114
+ const abortController = new AbortController();
115
+
116
+ setUploadingFiles((prev) => [
117
+ ...prev,
118
+ {
119
+ id: uploadId,
120
+ file,
121
+ progress: 0,
122
+ status: 'uploading',
123
+ abortController,
124
+ },
125
+ ]);
126
+
127
+ try {
128
+ const media = await uploadFile(file, {
129
+ signal: abortController.signal,
130
+ onProgress: (progress: UploadProgress) => {
131
+ setUploadingFiles((prev) =>
132
+ prev.map((f) =>
133
+ f.id === uploadId ? { ...f, progress: progress.percentage } : f
134
+ )
135
+ );
136
+ },
137
+ });
138
+
139
+ setUploadingFiles((prev) =>
140
+ prev.map((f) =>
141
+ f.id === uploadId ? { ...f, status: 'complete', progress: 100 } : f
142
+ )
143
+ );
144
+
145
+ onUploadComplete?.(media);
146
+
147
+ // Remove completed file after delay
148
+ setTimeout(() => {
149
+ setUploadingFiles((prev) => prev.filter((f) => f.id !== uploadId));
150
+ }, 2000);
151
+ } catch (error) {
152
+ if (error instanceof Error && error.name === 'AbortError') {
153
+ // Upload was cancelled
154
+ setUploadingFiles((prev) => prev.filter((f) => f.id !== uploadId));
155
+ } else {
156
+ setUploadingFiles((prev) =>
157
+ prev.map((f) =>
158
+ f.id === uploadId
159
+ ? { ...f, status: 'error', error: (error as Error).message }
160
+ : f
161
+ )
162
+ );
163
+ onUploadError?.(error as Error, file);
164
+ }
165
+ }
166
+ }
167
+ },
168
+ [accept, maxSize, onUploadComplete, onUploadError]
169
+ );
170
+
171
+ const handleDragOver = useCallback((e: React.DragEvent) => {
172
+ e.preventDefault();
173
+ e.stopPropagation();
174
+ setIsDragging(true);
175
+ }, []);
176
+
177
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
178
+ e.preventDefault();
179
+ e.stopPropagation();
180
+ setIsDragging(false);
181
+ }, []);
182
+
183
+ const handleDrop = useCallback(
184
+ (e: React.DragEvent) => {
185
+ e.preventDefault();
186
+ e.stopPropagation();
187
+ setIsDragging(false);
188
+
189
+ const files = e.dataTransfer.files;
190
+ if (files.length > 0) {
191
+ handleFiles(multiple ? files : [files[0]]);
192
+ }
193
+ },
194
+ [handleFiles, multiple]
195
+ );
196
+
197
+ const handleInputChange = useCallback(
198
+ (e: React.ChangeEvent<HTMLInputElement>) => {
199
+ const files = e.target.files;
200
+ if (files && files.length > 0) {
201
+ handleFiles(files);
202
+ }
203
+ // Reset input
204
+ e.target.value = '';
205
+ },
206
+ [handleFiles]
207
+ );
208
+
209
+ const handleClick = () => {
210
+ inputRef.current?.click();
211
+ };
212
+
213
+ const cancelUpload = (uploadId: string) => {
214
+ const upload = uploadingFiles.find((f) => f.id === uploadId);
215
+ if (upload) {
216
+ upload.abortController.abort();
217
+ }
218
+ };
219
+
220
+ const removeFile = (uploadId: string) => {
221
+ setUploadingFiles((prev) => prev.filter((f) => f.id !== uploadId));
222
+ };
223
+
224
+ return (
225
+ <div className={cn('space-y-4', className)}>
226
+ {/* Drop zone */}
227
+ <div
228
+ onDragOver={handleDragOver}
229
+ onDragLeave={handleDragLeave}
230
+ onDrop={handleDrop}
231
+ onClick={handleClick}
232
+ className={cn(
233
+ 'relative flex min-h-[200px] cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed p-6 transition-colors',
234
+ isDragging
235
+ ? 'border-primary bg-primary/5'
236
+ : 'border-muted-foreground/25 hover:border-primary/50 hover:bg-muted/50'
237
+ )}
238
+ >
239
+ <input
240
+ ref={inputRef}
241
+ type="file"
242
+ accept={accept?.join(',')}
243
+ multiple={multiple}
244
+ onChange={handleInputChange}
245
+ className="hidden"
246
+ />
247
+
248
+ <div className="flex flex-col items-center gap-2 text-center">
249
+ <div className="rounded-full bg-muted p-4">
250
+ <Upload className="h-8 w-8 text-muted-foreground" />
251
+ </div>
252
+ <div className="space-y-1">
253
+ <p className="text-sm font-medium">
254
+ Drop files here or click to upload
255
+ </p>
256
+ <p className="text-xs text-muted-foreground">
257
+ {accept?.join(', ') || 'Images, videos, audio, and PDFs'}
258
+ </p>
259
+ <p className="text-xs text-muted-foreground">
260
+ Max size: {formatFileSize(maxSize)}
261
+ </p>
262
+ </div>
263
+ </div>
264
+ </div>
265
+
266
+ {/* Upload progress list */}
267
+ {uploadingFiles.length > 0 && (
268
+ <div className="space-y-2">
269
+ {uploadingFiles.map((upload) => (
270
+ <div
271
+ key={upload.id}
272
+ className={cn(
273
+ 'flex items-center gap-3 rounded-lg border p-3',
274
+ upload.status === 'error' && 'border-destructive bg-destructive/5',
275
+ upload.status === 'complete' && 'border-green-500 bg-green-50'
276
+ )}
277
+ >
278
+ {getFileIcon(upload.file.type)}
279
+ <div className="flex-1 min-w-0">
280
+ <p className="truncate text-sm font-medium">{upload.file.name}</p>
281
+ <p className="text-xs text-muted-foreground">
282
+ {formatFileSize(upload.file.size)}
283
+ </p>
284
+ {upload.status === 'uploading' && (
285
+ <Progress value={upload.progress} className="mt-2 h-1" />
286
+ )}
287
+ {upload.status === 'error' && (
288
+ <p className="text-xs text-destructive mt-1">{upload.error}</p>
289
+ )}
290
+ {upload.status === 'complete' && (
291
+ <p className="text-xs text-green-600 mt-1">Upload complete</p>
292
+ )}
293
+ </div>
294
+ <Button
295
+ variant="ghost"
296
+ size="icon"
297
+ className="h-8 w-8 shrink-0"
298
+ onClick={(e) => {
299
+ e.stopPropagation();
300
+ if (upload.status === 'uploading') {
301
+ cancelUpload(upload.id);
302
+ } else {
303
+ removeFile(upload.id);
304
+ }
305
+ }}
306
+ >
307
+ <X className="h-4 w-4" />
308
+ </Button>
309
+ </div>
310
+ ))}
311
+ </div>
312
+ )}
313
+ </div>
314
+ );
315
+ }
316
+ `;
317
+ }
318
+ //# sourceMappingURL=media-uploader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-uploader.js","sourceRoot":"","sources":["../../../src/templates/media/media-uploader.ts"],"names":[],"mappings":";;AAYA,4DA6SC;AAzTD;;;;;;;;;;;GAWG;AACH,SAAgB,wBAAwB;IACtC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2SR,CAAC;AACF,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Generates the recent content component template.
3
+ *
4
+ * This component displays:
5
+ * - List of recently updated content items
6
+ * - Status badges
7
+ * - Content type labels
8
+ * - Time since last update
9
+ *
10
+ * @returns Template string for app/components/admin/recent-content.tsx
11
+ */
12
+ export declare function getRecentContentTemplate(): string;
13
+ //# sourceMappingURL=recent-content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recent-content.d.ts","sourceRoot":"","sources":["../../src/templates/recent-content.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CA0HjD"}