@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.
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/lib/cms-project.d.ts +127 -0
- package/lib/cms-project.d.ts.map +1 -0
- package/lib/cms-project.js +343 -0
- package/lib/cms-project.js.map +1 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +20 -0
- package/lib/index.js.map +1 -0
- package/lib/options.d.ts +59 -0
- package/lib/options.d.ts.map +1 -0
- package/lib/options.js +3 -0
- package/lib/options.js.map +1 -0
- package/lib/templates/admin-breadcrumbs.d.ts +13 -0
- package/lib/templates/admin-breadcrumbs.d.ts.map +1 -0
- package/lib/templates/admin-breadcrumbs.js +80 -0
- package/lib/templates/admin-breadcrumbs.js.map +1 -0
- package/lib/templates/admin-content-route.d.ts +18 -0
- package/lib/templates/admin-content-route.d.ts.map +1 -0
- package/lib/templates/admin-content-route.js +100 -0
- package/lib/templates/admin-content-route.js.map +1 -0
- package/lib/templates/admin-content-type-route.d.ts +9 -0
- package/lib/templates/admin-content-type-route.d.ts.map +1 -0
- package/lib/templates/admin-content-type-route.js +96 -0
- package/lib/templates/admin-content-type-route.js.map +1 -0
- package/lib/templates/admin-header.d.ts +13 -0
- package/lib/templates/admin-header.d.ts.map +1 -0
- package/lib/templates/admin-header.js +123 -0
- package/lib/templates/admin-header.js.map +1 -0
- package/lib/templates/admin-index.d.ts +9 -0
- package/lib/templates/admin-index.d.ts.map +1 -0
- package/lib/templates/admin-index.js +60 -0
- package/lib/templates/admin-index.js.map +1 -0
- package/lib/templates/admin-layout.d.ts +10 -0
- package/lib/templates/admin-layout.d.ts.map +1 -0
- package/lib/templates/admin-layout.js +46 -0
- package/lib/templates/admin-layout.js.map +1 -0
- package/lib/templates/admin-sidebar.d.ts +13 -0
- package/lib/templates/admin-sidebar.d.ts.map +1 -0
- package/lib/templates/admin-sidebar.js +149 -0
- package/lib/templates/admin-sidebar.js.map +1 -0
- package/lib/templates/content-editor.d.ts +10 -0
- package/lib/templates/content-editor.d.ts.map +1 -0
- package/lib/templates/content-editor.js +354 -0
- package/lib/templates/content-editor.js.map +1 -0
- package/lib/templates/content-schema.d.ts +10 -0
- package/lib/templates/content-schema.d.ts.map +1 -0
- package/lib/templates/content-schema.js +274 -0
- package/lib/templates/content-schema.js.map +1 -0
- package/lib/templates/content-table.d.ts +13 -0
- package/lib/templates/content-table.d.ts.map +1 -0
- package/lib/templates/content-table.js +177 -0
- package/lib/templates/content-table.js.map +1 -0
- package/lib/templates/content-types-examples.d.ts +19 -0
- package/lib/templates/content-types-examples.d.ts.map +1 -0
- package/lib/templates/content-types-examples.js +275 -0
- package/lib/templates/content-types-examples.js.map +1 -0
- package/lib/templates/content-types-registry.d.ts +10 -0
- package/lib/templates/content-types-registry.d.ts.map +1 -0
- package/lib/templates/content-types-registry.js +87 -0
- package/lib/templates/content-types-registry.js.map +1 -0
- package/lib/templates/content-types.d.ts +10 -0
- package/lib/templates/content-types.d.ts.map +1 -0
- package/lib/templates/content-types.js +384 -0
- package/lib/templates/content-types.js.map +1 -0
- package/lib/templates/dashboard-stats.d.ts +13 -0
- package/lib/templates/dashboard-stats.d.ts.map +1 -0
- package/lib/templates/dashboard-stats.js +117 -0
- package/lib/templates/dashboard-stats.js.map +1 -0
- package/lib/templates/editor/index.d.ts +6 -0
- package/lib/templates/editor/index.d.ts.map +1 -0
- package/lib/templates/editor/index.js +21 -0
- package/lib/templates/editor/index.js.map +1 -0
- package/lib/templates/editor/rich-text-editor.d.ts +7 -0
- package/lib/templates/editor/rich-text-editor.d.ts.map +1 -0
- package/lib/templates/editor/rich-text-editor.js +115 -0
- package/lib/templates/editor/rich-text-editor.js.map +1 -0
- package/lib/templates/editor/toolbar.d.ts +7 -0
- package/lib/templates/editor/toolbar.d.ts.map +1 -0
- package/lib/templates/editor/toolbar.js +272 -0
- package/lib/templates/editor/toolbar.js.map +1 -0
- package/lib/templates/form-fields/boolean-field.d.ts +7 -0
- package/lib/templates/form-fields/boolean-field.d.ts.map +1 -0
- package/lib/templates/form-fields/boolean-field.js +76 -0
- package/lib/templates/form-fields/boolean-field.js.map +1 -0
- package/lib/templates/form-fields/date-field.d.ts +7 -0
- package/lib/templates/form-fields/date-field.d.ts.map +1 -0
- package/lib/templates/form-fields/date-field.js +61 -0
- package/lib/templates/form-fields/date-field.js.map +1 -0
- package/lib/templates/form-fields/datetime-field.d.ts +7 -0
- package/lib/templates/form-fields/datetime-field.d.ts.map +1 -0
- package/lib/templates/form-fields/datetime-field.js +87 -0
- package/lib/templates/form-fields/datetime-field.js.map +1 -0
- package/lib/templates/form-fields/index.d.ts +23 -0
- package/lib/templates/form-fields/index.d.ts.map +1 -0
- package/lib/templates/form-fields/index.js +275 -0
- package/lib/templates/form-fields/index.js.map +1 -0
- package/lib/templates/form-fields/media-field.d.ts +10 -0
- package/lib/templates/form-fields/media-field.d.ts.map +1 -0
- package/lib/templates/form-fields/media-field.js +225 -0
- package/lib/templates/form-fields/media-field.js.map +1 -0
- package/lib/templates/form-fields/multiselect-field.d.ts +7 -0
- package/lib/templates/form-fields/multiselect-field.d.ts.map +1 -0
- package/lib/templates/form-fields/multiselect-field.js +121 -0
- package/lib/templates/form-fields/multiselect-field.js.map +1 -0
- package/lib/templates/form-fields/number-field.d.ts +7 -0
- package/lib/templates/form-fields/number-field.d.ts.map +1 -0
- package/lib/templates/form-fields/number-field.js +87 -0
- package/lib/templates/form-fields/number-field.js.map +1 -0
- package/lib/templates/form-fields/reference-field.d.ts +9 -0
- package/lib/templates/form-fields/reference-field.d.ts.map +1 -0
- package/lib/templates/form-fields/reference-field.js +145 -0
- package/lib/templates/form-fields/reference-field.js.map +1 -0
- package/lib/templates/form-fields/richtext-field.d.ts +9 -0
- package/lib/templates/form-fields/richtext-field.d.ts.map +1 -0
- package/lib/templates/form-fields/richtext-field.js +60 -0
- package/lib/templates/form-fields/richtext-field.js.map +1 -0
- package/lib/templates/form-fields/select-field.d.ts +7 -0
- package/lib/templates/form-fields/select-field.d.ts.map +1 -0
- package/lib/templates/form-fields/select-field.js +70 -0
- package/lib/templates/form-fields/select-field.js.map +1 -0
- package/lib/templates/form-fields/slug-field.d.ts +7 -0
- package/lib/templates/form-fields/slug-field.d.ts.map +1 -0
- package/lib/templates/form-fields/slug-field.js +143 -0
- package/lib/templates/form-fields/slug-field.js.map +1 -0
- package/lib/templates/form-fields/tags-field.d.ts +7 -0
- package/lib/templates/form-fields/tags-field.d.ts.map +1 -0
- package/lib/templates/form-fields/tags-field.js +172 -0
- package/lib/templates/form-fields/tags-field.js.map +1 -0
- package/lib/templates/form-fields/text-field.d.ts +7 -0
- package/lib/templates/form-fields/text-field.d.ts.map +1 -0
- package/lib/templates/form-fields/text-field.js +63 -0
- package/lib/templates/form-fields/text-field.js.map +1 -0
- package/lib/templates/form-fields/textarea-field.d.ts +7 -0
- package/lib/templates/form-fields/textarea-field.d.ts.map +1 -0
- package/lib/templates/form-fields/textarea-field.js +64 -0
- package/lib/templates/form-fields/textarea-field.js.map +1 -0
- package/lib/templates/index.d.ts +34 -0
- package/lib/templates/index.d.ts.map +1 -0
- package/lib/templates/index.js +92 -0
- package/lib/templates/index.js.map +1 -0
- package/lib/templates/media/index.d.ts +12 -0
- package/lib/templates/media/index.d.ts.map +1 -0
- package/lib/templates/media/index.js +50 -0
- package/lib/templates/media/index.js.map +1 -0
- package/lib/templates/media/media-api.d.ts +13 -0
- package/lib/templates/media/media-api.d.ts.map +1 -0
- package/lib/templates/media/media-api.js +274 -0
- package/lib/templates/media/media-api.js.map +1 -0
- package/lib/templates/media/media-grid.d.ts +14 -0
- package/lib/templates/media/media-grid.d.ts.map +1 -0
- package/lib/templates/media/media-grid.js +314 -0
- package/lib/templates/media/media-grid.js.map +1 -0
- package/lib/templates/media/media-library-route.d.ts +13 -0
- package/lib/templates/media/media-library-route.d.ts.map +1 -0
- package/lib/templates/media/media-library-route.js +105 -0
- package/lib/templates/media/media-library-route.js.map +1 -0
- package/lib/templates/media/media-picker.d.ts +13 -0
- package/lib/templates/media/media-picker.d.ts.map +1 -0
- package/lib/templates/media/media-picker.js +152 -0
- package/lib/templates/media/media-picker.js.map +1 -0
- package/lib/templates/media/media-uploader.d.ts +14 -0
- package/lib/templates/media/media-uploader.d.ts.map +1 -0
- package/lib/templates/media/media-uploader.js +318 -0
- package/lib/templates/media/media-uploader.js.map +1 -0
- package/lib/templates/recent-content.d.ts +13 -0
- package/lib/templates/recent-content.d.ts.map +1 -0
- package/lib/templates/recent-content.js +138 -0
- package/lib/templates/recent-content.js.map +1 -0
- package/lib/templates/slug-utils.d.ts +10 -0
- package/lib/templates/slug-utils.d.ts.map +1 -0
- package/lib/templates/slug-utils.js +194 -0
- package/lib/templates/slug-utils.js.map +1 -0
- package/lib/templates/ui-avatar.d.ts +8 -0
- package/lib/templates/ui-avatar.d.ts.map +1 -0
- package/lib/templates/ui-avatar.js +60 -0
- package/lib/templates/ui-avatar.js.map +1 -0
- package/lib/templates/ui-badge.d.ts +8 -0
- package/lib/templates/ui-badge.d.ts.map +1 -0
- package/lib/templates/ui-badge.js +52 -0
- package/lib/templates/ui-badge.js.map +1 -0
- package/lib/templates/ui-dialog.d.ts +10 -0
- package/lib/templates/ui-dialog.d.ts.map +1 -0
- package/lib/templates/ui-dialog.js +134 -0
- package/lib/templates/ui-dialog.js.map +1 -0
- package/lib/templates/ui-dropdown-menu.d.ts +8 -0
- package/lib/templates/ui-dropdown-menu.d.ts.map +1 -0
- package/lib/templates/ui-dropdown-menu.js +210 -0
- package/lib/templates/ui-dropdown-menu.js.map +1 -0
- package/lib/templates/ui-popover.d.ts +8 -0
- package/lib/templates/ui-popover.d.ts.map +1 -0
- package/lib/templates/ui-popover.js +43 -0
- package/lib/templates/ui-popover.js.map +1 -0
- package/lib/templates/ui-progress.d.ts +10 -0
- package/lib/templates/ui-progress.d.ts.map +1 -0
- package/lib/templates/ui-progress.js +40 -0
- package/lib/templates/ui-progress.js.map +1 -0
- package/lib/templates/ui-table.d.ts +8 -0
- package/lib/templates/ui-table.d.ts.map +1 -0
- package/lib/templates/ui-table.js +129 -0
- package/lib/templates/ui-table.js.map +1 -0
- package/lib/templates/ui-tabs.d.ts +10 -0
- package/lib/templates/ui-tabs.d.ts.map +1 -0
- package/lib/templates/ui-tabs.js +67 -0
- package/lib/templates/ui-tabs.js.map +1 -0
- 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"}
|