convex-cms 0.0.5-alpha.3 → 0.0.5-alpha.5
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/README.md +47 -12
- package/admin/src/components/BulkActionBar.tsx +4 -4
- package/admin/src/components/ContentEntryEditor.tsx +8 -28
- package/admin/src/components/ContentTypeFormModal.tsx +2 -2
- package/admin/src/components/Sidebar.tsx +150 -58
- package/admin/src/components/TaxonomyEditor.tsx +2 -2
- package/admin/src/components/TermTree.tsx +5 -5
- package/admin/src/components/VersionCompare.tsx +1 -1
- package/admin/src/components/VersionHistory.tsx +2 -2
- package/admin/src/components/fields/CategoryField.tsx +1 -1
- package/admin/src/components/fields/MediaField.tsx +4 -4
- package/admin/src/components/fields/ReferenceField.tsx +4 -4
- package/admin/src/components/fields/TagField.tsx +3 -3
- package/admin/src/components/filters/TaxonomyFilter.tsx +2 -2
- package/admin/src/components/media/MediaAssetEditDialog.tsx +2 -2
- package/admin/src/components/media/MediaFolderEditDialog.tsx +1 -1
- package/admin/src/components/media/MediaMoveModal.tsx +2 -2
- package/admin/src/components/media/MediaTaxonomyPicker.tsx +4 -4
- package/admin/src/components/ui/collapsible.tsx +7 -0
- package/admin/src/contexts/SettingsConfigContext.tsx +2 -2
- package/admin/src/embed/components/EmbedSidebar.tsx +163 -68
- package/admin/src/pages/MediaPage.tsx +1102 -8
- package/admin/src/pages/SettingsPage.tsx +171 -108
- package/admin/src/routes/entries/$entryId.tsx +2 -2
- package/admin/src/routes/entries/new.$contentTypeId.tsx +1 -1
- package/admin/src/routes/entries/type/$contentTypeId.tsx +6 -6
- package/admin/src/routes/media.tsx +23 -1094
- package/admin-dist/nitro.json +1 -1
- package/admin-dist/public/assets/{CmsEmptyState-CkqBIab3.js → CmsEmptyState-Do_erIgn.js} +1 -1
- package/admin-dist/public/assets/{CmsPageHeader-CUtl5MMG.js → CmsPageHeader-qDwPGi48.js} +1 -1
- package/admin-dist/public/assets/{CmsStatusBadge-CUYFgEe-.js → CmsStatusBadge-Dd9uToHE.js} +1 -1
- package/admin-dist/public/assets/{CmsSurface-CsJfAVa3.js → CmsSurface-DBy5Lumx.js} +1 -1
- package/admin-dist/public/assets/{CmsToolbar-CnfbcxeP.js → CmsToolbar-D1-Y-7SK.js} +1 -1
- package/admin-dist/public/assets/ContentEntryEditor-CWBiIx52.js +4 -0
- package/admin-dist/public/assets/{TaxonomyFilter-CWCxC5HZ.js → TaxonomyFilter-CdYQawxb.js} +1 -1
- package/admin-dist/public/assets/_contentTypeId-D9VMP6Gs.js +1 -0
- package/admin-dist/public/assets/_entryId-2FlCfqE7.js +1 -0
- package/admin-dist/public/assets/{alert-CF1BSzGR.js → alert-GxZx0y5c.js} +1 -1
- package/admin-dist/public/assets/{badge-CmuOIVKp.js → badge-BAlGIjop.js} +1 -1
- package/admin-dist/public/assets/{circle-check-big-BKDVG6DU.js → circle-check-big-CpLxAvEj.js} +1 -1
- package/admin-dist/public/assets/{command-XJxnF2Sd.js → command-di7XCqcv.js} +1 -1
- package/admin-dist/public/assets/content-D8zELsDG.js +1 -0
- package/admin-dist/public/assets/{content-types-CrNEm8Hf.js → content-types-BmzD0krT.js} +2 -2
- package/admin-dist/public/assets/globals-BvFfH-v9.css +1 -0
- package/admin-dist/public/assets/{index-C7xOwudI.js → index-zqfj4T_v.js} +1 -1
- package/admin-dist/public/assets/{label-CHCnXeBk.js → label-B6PPtKR5.js} +1 -1
- package/admin-dist/public/assets/{link-2-Bb34judH.js → link-2-W2fVnVOf.js} +1 -1
- package/admin-dist/public/assets/{list-9Pzt48ld.js → list-F8O0lZXC.js} +1 -1
- package/admin-dist/public/assets/main-dZT72bAG.js +97 -0
- package/admin-dist/public/assets/media-CETueFbV.js +1 -0
- package/admin-dist/public/assets/new._contentTypeId-BV2-TyyR.js +1 -0
- package/admin-dist/public/assets/{plus-Ceef7DHk.js → plus-AABQIF0N.js} +1 -1
- package/admin-dist/public/assets/{rotate-ccw-7k7-4VUq.js → rotate-ccw-BZpZtw0N.js} +1 -1
- package/admin-dist/public/assets/{scroll-area-CC6wujnp.js → scroll-area-CDfk-zrz.js} +1 -1
- package/admin-dist/public/assets/{search-DwoUV2pv.js → search-BvgYr-c9.js} +1 -1
- package/admin-dist/public/assets/{select-hOZTp8aC.js → select-BuiHcMzS.js} +1 -1
- package/admin-dist/public/assets/settings-DBxbYDvn.js +1 -0
- package/admin-dist/public/assets/{switch-jX2pDaNU.js → switch-DiJvolcs.js} +1 -1
- package/admin-dist/public/assets/tabs-Cgz6G_Xy.js +1 -0
- package/admin-dist/public/assets/{tanstack-adapter-B-Glm4kH.js → tanstack-adapter-BknsSgra.js} +1 -1
- package/admin-dist/public/assets/taxonomies-DOErsLl5.js +1 -0
- package/admin-dist/public/assets/{textarea-B6SfBmr0.js → textarea-CgggMxUX.js} +1 -1
- package/admin-dist/public/assets/{trash-BOCnIznD.js → trash-BU4ANuaW.js} +1 -1
- package/admin-dist/public/assets/{triangle-alert-CXFIO_Gu.js → triangle-alert-lvCbwp0s.js} +1 -1
- package/admin-dist/public/assets/{useBreadcrumbLabel-_6qBagc3.js → useBreadcrumbLabel-D00rvqjw.js} +1 -1
- package/admin-dist/public/assets/{usePermissions-M1ijZ7a6.js → usePermissions-D7tQowaF.js} +1 -1
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-collapsible.mjs +144 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-slot.mjs +21 -21
- package/admin-dist/server/_libs/lucide-react.mjs +131 -124
- package/admin-dist/server/_ssr/{CmsButton-DOiTVKQq.mjs → CmsButton-DbzfJru_.mjs} +1 -1
- package/admin-dist/server/_ssr/{CmsEmptyState-fbnGt3LD.mjs → CmsEmptyState-CuvcXr3Z.mjs} +3 -3
- package/admin-dist/server/_ssr/{CmsPageHeader-DHRrdOZa.mjs → CmsPageHeader-ClNPU7Up.mjs} +1 -1
- package/admin-dist/server/_ssr/{CmsStatusBadge-s7obWbKZ.mjs → CmsStatusBadge-CojMbrY7.mjs} +2 -2
- package/admin-dist/server/_ssr/{CmsSurface-rFoYjb62.mjs → CmsSurface-Dcv440rp.mjs} +1 -1
- package/admin-dist/server/_ssr/{CmsToolbar-zTE45z2q.mjs → CmsToolbar-BKv1nL6u.mjs} +2 -2
- package/admin-dist/server/_ssr/{ContentEntryEditor-BLoEjT_m.mjs → ContentEntryEditor-weiXSBdZ.mjs} +35 -51
- package/admin-dist/server/_ssr/{TaxonomyFilter-XAtaJC2z.mjs → TaxonomyFilter-BPQ57Mwk.mjs} +7 -7
- package/admin-dist/server/_ssr/{_contentTypeId-Csl4822C.mjs → _contentTypeId-DyyauLOs.mjs} +24 -23
- package/admin-dist/server/_ssr/{_entryId-D8alLFBx.mjs → _entryId-9Cafwxmw.mjs} +22 -21
- package/admin-dist/server/_ssr/_tanstack-start-manifest_v-Dk-FIYPN.mjs +4 -0
- package/admin-dist/server/_ssr/{command-C0Di14--.mjs → command-CEf8YBxY.mjs} +1 -1
- package/admin-dist/server/_ssr/{content-CT-FPsmV.mjs → content-ZFWVzO25.mjs} +20 -19
- package/admin-dist/server/_ssr/{content-types-C8cBFdzE.mjs → content-types-D25lUE-j.mjs} +18 -17
- package/admin-dist/server/_ssr/{index-BJtcrEc-.mjs → index-BlSIlH4Z.mjs} +10 -9
- package/admin-dist/server/_ssr/index.mjs +2 -2
- package/admin-dist/server/_ssr/{label-qn2Afwl4.mjs → label-PblVvdRv.mjs} +1 -1
- package/admin-dist/server/_ssr/{media-qv5IAsMZ.mjs → media-CD2_NUMw.mjs} +683 -314
- package/admin-dist/server/_ssr/{new._contentTypeId-DdGyrhqs.mjs → new._contentTypeId-dmZy6PBX.mjs} +20 -19
- package/admin-dist/server/_ssr/{router-nSVkxb6Y.mjs → router-x6Ab8T4s.mjs} +109 -43
- package/admin-dist/server/_ssr/{scroll-area-BCinP455.mjs → scroll-area-BH_1K-WT.mjs} +1 -1
- package/admin-dist/server/_ssr/{select-BKQlQScw.mjs → select-CrfEkFJw.mjs} +2 -2
- package/admin-dist/server/_ssr/{settings-BCr2KQlk.mjs → settings-DVdsoWoh.mjs} +158 -105
- package/admin-dist/server/_ssr/{switch-BaOi42fE.mjs → switch-DX_X8vZl.mjs} +1 -1
- package/admin-dist/server/_ssr/{tabs-DYXEi9kq.mjs → tabs-4FWM0sn8.mjs} +3 -3
- package/admin-dist/server/_ssr/{tanstack-adapter-Bsz8kha-.mjs → tanstack-adapter-D3ZcKtbY.mjs} +1 -1
- package/admin-dist/server/_ssr/{taxonomies-CueMHTbE.mjs → taxonomies-BHFfO9Yr.mjs} +21 -20
- package/admin-dist/server/_ssr/{textarea-CI0Jqx2x.mjs → textarea-CZVaroMc.mjs} +1 -1
- package/admin-dist/server/_ssr/{trash-DE6W8GoX.mjs → trash-9tUB2KwI.mjs} +14 -13
- package/admin-dist/server/_ssr/{useBreadcrumbLabel-B5Yi72lM.mjs → useBreadcrumbLabel-DVme3DSb.mjs} +1 -1
- package/admin-dist/server/_ssr/{usePermissions-C3nZ-Izm.mjs → usePermissions-zAQj-ruE.mjs} +1 -1
- package/admin-dist/server/index.mjs +188 -188
- package/dist/cli/commands/init.d.ts +12 -2
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +136 -138
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/templates/admin.d.ts +10 -0
- package/dist/cli/templates/admin.d.ts.map +1 -0
- package/dist/cli/templates/admin.js +212 -0
- package/dist/cli/templates/admin.js.map +1 -0
- package/dist/cli/templates/cmsClient.d.ts +7 -0
- package/dist/cli/templates/cmsClient.d.ts.map +1 -0
- package/dist/cli/templates/cmsClient.js +36 -0
- package/dist/cli/templates/cmsClient.js.map +1 -0
- package/dist/cli/templates/cmsConfig.d.ts +7 -0
- package/dist/cli/templates/cmsConfig.d.ts.map +1 -0
- package/dist/cli/templates/cmsConfig.js +86 -0
- package/dist/cli/templates/cmsConfig.js.map +1 -0
- package/dist/cli/templates/index.d.ts +10 -0
- package/dist/cli/templates/index.d.ts.map +1 -0
- package/dist/cli/templates/index.js +10 -0
- package/dist/cli/templates/index.js.map +1 -0
- package/dist/cli/templates/schemas/blog.d.ts +8 -0
- package/dist/cli/templates/schemas/blog.d.ts.map +1 -0
- package/dist/cli/templates/schemas/blog.js +103 -0
- package/dist/cli/templates/schemas/blog.js.map +1 -0
- package/dist/cli/templates/schemas/docs.d.ts +8 -0
- package/dist/cli/templates/schemas/docs.d.ts.map +1 -0
- package/dist/cli/templates/schemas/docs.js +110 -0
- package/dist/cli/templates/schemas/docs.js.map +1 -0
- package/dist/cli/templates/schemas/index.d.ts +11 -0
- package/dist/cli/templates/schemas/index.d.ts.map +1 -0
- package/dist/cli/templates/schemas/index.js +13 -0
- package/dist/cli/templates/schemas/index.js.map +1 -0
- package/dist/cli/templates/schemas/landing.d.ts +8 -0
- package/dist/cli/templates/schemas/landing.d.ts.map +1 -0
- package/dist/cli/templates/schemas/landing.js +135 -0
- package/dist/cli/templates/schemas/landing.js.map +1 -0
- package/dist/cli/utils/fileUtils.d.ts +21 -0
- package/dist/cli/utils/fileUtils.d.ts.map +1 -0
- package/dist/cli/utils/fileUtils.js +95 -0
- package/dist/cli/utils/fileUtils.js.map +1 -0
- package/dist/cli/utils/prompts.d.ts +25 -0
- package/dist/cli/utils/prompts.d.ts.map +1 -0
- package/dist/cli/utils/prompts.js +87 -0
- package/dist/cli/utils/prompts.js.map +1 -0
- package/dist/client/admin/index.d.ts +75 -2
- package/dist/client/admin/index.d.ts.map +1 -1
- package/dist/client/admin/index.js +17 -1
- package/dist/client/admin/index.js.map +1 -1
- package/dist/client/admin/media.d.ts.map +1 -1
- package/dist/client/admin/media.js +3 -3
- package/dist/client/admin/media.js.map +1 -1
- package/dist/client/admin/settings.d.ts +50 -0
- package/dist/client/admin/settings.d.ts.map +1 -0
- package/dist/client/admin/settings.js +89 -0
- package/dist/client/admin/settings.js.map +1 -0
- package/dist/client/admin/taxonomies.d.ts.map +1 -1
- package/dist/client/admin/trash.d.ts.map +1 -1
- package/dist/client/admin/types.d.ts +40 -0
- package/dist/client/admin/types.d.ts.map +1 -1
- package/dist/client/admin/validators.d.ts +243 -3
- package/dist/client/admin/validators.d.ts.map +1 -1
- package/dist/client/admin/validators.js +18 -0
- package/dist/client/admin/validators.js.map +1 -1
- package/dist/client/adminConfig.d.ts +1 -1
- package/dist/client/agentTools.d.ts +1 -1427
- package/dist/client/agentTools.d.ts.map +1 -1
- package/dist/client/config.d.ts +145 -0
- package/dist/client/config.d.ts.map +1 -0
- package/dist/client/config.js +132 -0
- package/dist/client/config.js.map +1 -0
- package/dist/client/index.d.ts +3 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +13 -6
- package/dist/client/index.js.map +1 -1
- package/dist/client/schema/defineContentType.js +1 -1
- package/dist/client/schema/defineContentType.js.map +1 -1
- package/dist/client/wrapper.d.ts.map +1 -1
- package/dist/client/wrapper.js +0 -4
- package/dist/client/wrapper.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +42 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/contentEntries.d.ts.map +1 -1
- package/dist/component/contentEntries.js +0 -2
- package/dist/component/contentEntries.js.map +1 -1
- package/dist/component/contentEntryMutations.d.ts.map +1 -1
- package/dist/component/contentEntryMutations.js +0 -1
- package/dist/component/contentEntryMutations.js.map +1 -1
- package/dist/component/contentLock.d.ts.map +1 -1
- package/dist/component/contentLock.js +0 -2
- package/dist/component/contentLock.js.map +1 -1
- package/dist/component/contentTypeMutations.d.ts +1 -1
- package/dist/component/index.d.ts +2 -1
- package/dist/component/index.d.ts.map +1 -1
- package/dist/component/index.js +3 -1
- package/dist/component/index.js.map +1 -1
- package/dist/component/schema.d.ts +18 -1
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +6 -1
- package/dist/component/schema.js.map +1 -1
- package/dist/component/settings.d.ts +60 -0
- package/dist/component/settings.d.ts.map +1 -0
- package/dist/component/settings.js +126 -0
- package/dist/component/settings.js.map +1 -0
- package/dist/component/validators.d.ts +36 -0
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +15 -0
- package/dist/component/validators.js.map +1 -1
- package/dist/test.d.ts +16 -2
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +2 -6
- package/dist/test.js.map +1 -1
- package/package.json +31 -22
- package/admin/README.md +0 -99
- package/admin-dist/public/assets/ContentEntryEditor-BU220CCy.js +0 -4
- package/admin-dist/public/assets/_contentTypeId-DK8cskRt.js +0 -1
- package/admin-dist/public/assets/_entryId-CuVMExbb.js +0 -1
- package/admin-dist/public/assets/content-QBUxdxbS.js +0 -1
- package/admin-dist/public/assets/globals-B7Wsfh_v.css +0 -1
- package/admin-dist/public/assets/main-CjQ2VI9L.js +0 -97
- package/admin-dist/public/assets/media-Dc5PWt2Q.js +0 -1
- package/admin-dist/public/assets/new._contentTypeId-C_I4YxIa.js +0 -1
- package/admin-dist/public/assets/settings-t2PbCZh4.js +0 -1
- package/admin-dist/public/assets/tabs-q4EbZk7c.js +0 -1
- package/admin-dist/public/assets/taxonomies-kyk5P4ZW.js +0 -1
- package/admin-dist/server/_ssr/_tanstack-start-manifest_v-BffZedId.mjs +0 -4
|
@@ -1,1095 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
Select,
|
|
25
|
-
SelectContent,
|
|
26
|
-
SelectItem,
|
|
27
|
-
SelectTrigger,
|
|
28
|
-
SelectValue,
|
|
29
|
-
} from '~/components/ui/select'
|
|
30
|
-
import { Checkbox } from '~/components/ui/checkbox'
|
|
31
|
-
import { cn } from '~/lib/cn'
|
|
32
|
-
import {
|
|
33
|
-
Image,
|
|
34
|
-
Video,
|
|
35
|
-
Music,
|
|
36
|
-
FileText,
|
|
37
|
-
File,
|
|
38
|
-
Folder,
|
|
39
|
-
Home,
|
|
40
|
-
ChevronLeft,
|
|
41
|
-
FolderPlus,
|
|
42
|
-
Upload,
|
|
43
|
-
Search,
|
|
44
|
-
X,
|
|
45
|
-
Trash2,
|
|
46
|
-
RotateCcw,
|
|
47
|
-
} from 'lucide-react'
|
|
48
|
-
import {
|
|
49
|
-
MediaPreviewModal,
|
|
50
|
-
type MediaAsset,
|
|
51
|
-
} from '~/components/media/MediaPreviewModal'
|
|
52
|
-
import {
|
|
53
|
-
MediaAssetEditDialog,
|
|
54
|
-
type MediaAssetForEdit,
|
|
55
|
-
} from '~/components/media/MediaAssetEditDialog'
|
|
56
|
-
import {
|
|
57
|
-
MediaFolderEditDialog,
|
|
58
|
-
type MediaFolderForEdit,
|
|
59
|
-
} from '~/components/media/MediaFolderEditDialog'
|
|
60
|
-
import { MediaAssetActions } from '~/components/media/MediaAssetActions'
|
|
61
|
-
import { MediaFolderActions } from '~/components/media/MediaFolderActions'
|
|
62
|
-
import { MediaBulkActionBar } from '~/components/media/MediaBulkActionBar'
|
|
63
|
-
import { MediaTrashBulkActionBar } from '~/components/media/MediaTrashBulkActionBar'
|
|
64
|
-
import { MediaMoveModal } from '~/components/media/MediaMoveModal'
|
|
65
|
-
import { CmsConfirmDialog } from '~/components/cmsds/CmsDialog'
|
|
66
|
-
import { Tabs, TabsList, TabsTrigger } from '~/components/ui/tabs'
|
|
67
|
-
import { Badge } from '~/components/ui/badge'
|
|
68
|
-
|
|
69
|
-
type MediaView = 'library' | 'trash'
|
|
70
|
-
|
|
71
|
-
export const Route = createFileRoute('/media')({
|
|
72
|
-
component: MediaPage,
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
function formatFileSize(bytes: number): string {
|
|
76
|
-
if (bytes === 0) return '0 B'
|
|
77
|
-
const k = 1024
|
|
78
|
-
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
79
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
80
|
-
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function formatDate(timestamp: number): string {
|
|
84
|
-
return new Date(timestamp).toLocaleDateString('en-US', {
|
|
85
|
-
month: 'short',
|
|
86
|
-
day: 'numeric',
|
|
87
|
-
year: 'numeric',
|
|
88
|
-
})
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function getMediaTypeIcon(type: string, className = 'size-6') {
|
|
92
|
-
const iconProps = { className }
|
|
93
|
-
switch (type) {
|
|
94
|
-
case 'image':
|
|
95
|
-
return <Image {...iconProps} />
|
|
96
|
-
case 'video':
|
|
97
|
-
return <Video {...iconProps} />
|
|
98
|
-
case 'audio':
|
|
99
|
-
return <Music {...iconProps} />
|
|
100
|
-
case 'document':
|
|
101
|
-
return <FileText {...iconProps} />
|
|
102
|
-
default:
|
|
103
|
-
return <File {...iconProps} />
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
type MediaType = 'image' | 'video' | 'audio' | 'document' | 'other'
|
|
108
|
-
|
|
109
|
-
function getMediaTypeFromMimeType(mimeType?: string): MediaType {
|
|
110
|
-
if (!mimeType) return 'other'
|
|
111
|
-
if (mimeType.startsWith('image/')) return 'image'
|
|
112
|
-
if (mimeType.startsWith('video/')) return 'video'
|
|
113
|
-
if (mimeType.startsWith('audio/')) return 'audio'
|
|
114
|
-
if (
|
|
115
|
-
mimeType === 'application/pdf' ||
|
|
116
|
-
mimeType.includes('document') ||
|
|
117
|
-
mimeType.includes('sheet') ||
|
|
118
|
-
mimeType.includes('presentation') ||
|
|
119
|
-
mimeType.startsWith('text/')
|
|
120
|
-
) {
|
|
121
|
-
return 'document'
|
|
122
|
-
}
|
|
123
|
-
return 'other'
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function MediaPage() {
|
|
127
|
-
const { settings } = useSettingsConfig()
|
|
128
|
-
const navigate = useNavigate()
|
|
129
|
-
|
|
130
|
-
useEffect(() => {
|
|
131
|
-
if (settings && !settings.features.mediaManagement) {
|
|
132
|
-
navigate({ to: '/' })
|
|
133
|
-
}
|
|
134
|
-
}, [settings, navigate])
|
|
135
|
-
|
|
136
|
-
const [currentFolderId, setCurrentFolderId] = useState<
|
|
137
|
-
string | undefined
|
|
138
|
-
>(undefined)
|
|
139
|
-
const [searchQuery, setSearchQuery] = useState('')
|
|
140
|
-
const [typeFilter, setTypeFilter] = useState<MediaType | ''>('')
|
|
141
|
-
const [selectedTermIds, setSelectedTermIds] = useState<string[]>([])
|
|
142
|
-
const [selectedAssets, setSelectedAssets] = useState<Set<string>>(
|
|
143
|
-
new Set()
|
|
144
|
-
)
|
|
145
|
-
const [isSelectionMode, setIsSelectionMode] = useState(false)
|
|
146
|
-
const [showNewFolderModal, setShowNewFolderModal] = useState(false)
|
|
147
|
-
const [showUploadModal, setShowUploadModal] = useState(false)
|
|
148
|
-
const [newFolderName, setNewFolderName] = useState('')
|
|
149
|
-
const [isCreatingFolder, setIsCreatingFolder] = useState(false)
|
|
150
|
-
const [folderError, setFolderError] = useState('')
|
|
151
|
-
const [previewIndex, setPreviewIndex] = useState<number | null>(null)
|
|
152
|
-
const [editingAsset, setEditingAsset] = useState<MediaAssetForEdit | null>(null)
|
|
153
|
-
const [editingFolder, setEditingFolder] = useState<MediaFolderForEdit | null>(null)
|
|
154
|
-
const [deleteTarget, setDeleteTarget] = useState<{
|
|
155
|
-
type: 'asset' | 'folder'
|
|
156
|
-
id: string
|
|
157
|
-
name: string
|
|
158
|
-
} | null>(null)
|
|
159
|
-
const [isDeleting, setIsDeleting] = useState(false)
|
|
160
|
-
const [showMoveModal, setShowMoveModal] = useState(false)
|
|
161
|
-
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false)
|
|
162
|
-
const [isBulkDeleting, setIsBulkDeleting] = useState(false)
|
|
163
|
-
const [activeView, setActiveView] = useState<MediaView>('library')
|
|
164
|
-
const [isRestoring, setIsRestoring] = useState(false)
|
|
165
|
-
const [isPermanentlyDeleting, setIsPermanentlyDeleting] = useState(false)
|
|
166
|
-
const [showPermanentDeleteConfirm, setShowPermanentDeleteConfirm] = useState(false)
|
|
167
|
-
const [permanentDeleteTarget, setPermanentDeleteTarget] = useState<string | 'bulk' | null>(null)
|
|
168
|
-
|
|
169
|
-
const isTrashView = activeView === 'trash'
|
|
170
|
-
|
|
171
|
-
const assetsResult = useQuery(api.media.listAssets, {
|
|
172
|
-
folderId: isTrashView ? undefined : currentFolderId,
|
|
173
|
-
type: typeFilter || undefined,
|
|
174
|
-
search: searchQuery || undefined,
|
|
175
|
-
deletedOnly: isTrashView ? true : undefined,
|
|
176
|
-
paginationOpts: { numItems: 100, cursor: null },
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
const trashCount = useQuery(api.media.getTrashCount, {})
|
|
180
|
-
|
|
181
|
-
const mediaByTermResult0 = useQuery(
|
|
182
|
-
api.taxonomies.getMediaByTerm,
|
|
183
|
-
selectedTermIds[0] ? { termId: selectedTermIds[0] } : 'skip'
|
|
184
|
-
)
|
|
185
|
-
const mediaByTermResult1 = useQuery(
|
|
186
|
-
api.taxonomies.getMediaByTerm,
|
|
187
|
-
selectedTermIds[1] ? { termId: selectedTermIds[1] } : 'skip'
|
|
188
|
-
)
|
|
189
|
-
const mediaByTermResult2 = useQuery(
|
|
190
|
-
api.taxonomies.getMediaByTerm,
|
|
191
|
-
selectedTermIds[2] ? { termId: selectedTermIds[2] } : 'skip'
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
const termFilteredMediaIds = useMemo(() => {
|
|
195
|
-
if (selectedTermIds.length === 0) return null
|
|
196
|
-
const ids = new Set<string>()
|
|
197
|
-
const results = [mediaByTermResult0, mediaByTermResult1, mediaByTermResult2]
|
|
198
|
-
for (let i = 0; i < selectedTermIds.length && i < 3; i++) {
|
|
199
|
-
const result = results[i]
|
|
200
|
-
if (result?.page) {
|
|
201
|
-
for (const mediaId of result.page) {
|
|
202
|
-
ids.add(mediaId)
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
return ids
|
|
207
|
-
}, [selectedTermIds, mediaByTermResult0, mediaByTermResult1, mediaByTermResult2])
|
|
208
|
-
|
|
209
|
-
const folders = useQuery(api.media.listFolders, {
|
|
210
|
-
parentId: isTrashView ? undefined : currentFolderId,
|
|
211
|
-
deletedOnly: isTrashView || undefined,
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
const currentFolder = useQuery(
|
|
215
|
-
api.media.getFolder,
|
|
216
|
-
currentFolderId ? { id: currentFolderId } : 'skip'
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
const folderTree = useQuery(api.media.getFolderTree, {})
|
|
220
|
-
|
|
221
|
-
const createFolder = useMutation(api.media.createFolder)
|
|
222
|
-
const deleteAsset = useMutation(api.media.deleteAsset)
|
|
223
|
-
const deleteFolder = useMutation(api.media.deleteFolder)
|
|
224
|
-
const restoreAsset = useMutation(api.media.restoreAsset)
|
|
225
|
-
const restoreFolder = useMutation(api.media.restoreFolder)
|
|
226
|
-
const permanentDeleteAsset = useMutation(api.media.permanentDeleteAsset)
|
|
227
|
-
const bulkPermanentDeleteAssets = useMutation(api.media.bulkPermanentDeleteAssets)
|
|
228
|
-
|
|
229
|
-
const breadcrumbPath = useMemo(() => {
|
|
230
|
-
if (!currentFolderId || !folderTree) return []
|
|
231
|
-
|
|
232
|
-
type FolderItem = (typeof folderTree)[number]
|
|
233
|
-
const path: FolderItem[] = []
|
|
234
|
-
let folder: FolderItem | undefined = folderTree.find(
|
|
235
|
-
(f) => f._id === currentFolderId
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
while (folder) {
|
|
239
|
-
path.unshift(folder)
|
|
240
|
-
const parentId = folder.parentId
|
|
241
|
-
folder = parentId ? folderTree.find((f) => f._id === parentId) : undefined
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return path
|
|
245
|
-
}, [currentFolderId, folderTree])
|
|
246
|
-
|
|
247
|
-
const handleFolderClick = useCallback((folderId: string) => {
|
|
248
|
-
setCurrentFolderId(folderId)
|
|
249
|
-
setSearchQuery('')
|
|
250
|
-
}, [])
|
|
251
|
-
|
|
252
|
-
const handleNavigateUp = useCallback(() => {
|
|
253
|
-
if (currentFolder?.parentId) {
|
|
254
|
-
setCurrentFolderId(currentFolder.parentId as string)
|
|
255
|
-
} else {
|
|
256
|
-
setCurrentFolderId(undefined)
|
|
257
|
-
}
|
|
258
|
-
}, [currentFolder])
|
|
259
|
-
|
|
260
|
-
const handleNavigateToRoot = useCallback(() => {
|
|
261
|
-
setCurrentFolderId(undefined)
|
|
262
|
-
setSearchQuery('')
|
|
263
|
-
}, [])
|
|
264
|
-
|
|
265
|
-
const handleAssetSelect = useCallback((assetId: string) => {
|
|
266
|
-
setSelectedAssets((prev) => {
|
|
267
|
-
const next = new Set(prev)
|
|
268
|
-
if (next.has(assetId)) {
|
|
269
|
-
next.delete(assetId)
|
|
270
|
-
} else {
|
|
271
|
-
next.add(assetId)
|
|
272
|
-
}
|
|
273
|
-
return next
|
|
274
|
-
})
|
|
275
|
-
}, [])
|
|
276
|
-
|
|
277
|
-
const handleSelectAll = useCallback(() => {
|
|
278
|
-
if (!assetsResult?.page) return
|
|
279
|
-
setSelectedAssets(
|
|
280
|
-
new Set(assetsResult.page.map((a) => a._id as string))
|
|
281
|
-
)
|
|
282
|
-
}, [assetsResult?.page])
|
|
283
|
-
|
|
284
|
-
const handleDeselectAll = useCallback(() => {
|
|
285
|
-
setSelectedAssets(new Set())
|
|
286
|
-
}, [])
|
|
287
|
-
|
|
288
|
-
const handleViewChange = useCallback((view: MediaView) => {
|
|
289
|
-
setActiveView(view)
|
|
290
|
-
setSelectedAssets(new Set())
|
|
291
|
-
setIsSelectionMode(false)
|
|
292
|
-
}, [])
|
|
293
|
-
|
|
294
|
-
const handleAssetClick = useCallback(
|
|
295
|
-
(assetId: string) => {
|
|
296
|
-
if (isSelectionMode) {
|
|
297
|
-
handleAssetSelect(assetId)
|
|
298
|
-
} else {
|
|
299
|
-
const index = assetsResult?.page?.findIndex((a) => a._id === assetId) ?? -1
|
|
300
|
-
if (index !== -1) {
|
|
301
|
-
setPreviewIndex(index)
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
},
|
|
305
|
-
[isSelectionMode, handleAssetSelect, assetsResult?.page]
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
const handlePreviewNavigate = useCallback((index: number) => {
|
|
309
|
-
setPreviewIndex(index)
|
|
310
|
-
}, [])
|
|
311
|
-
|
|
312
|
-
const handleDelete = useCallback(async () => {
|
|
313
|
-
if (!deleteTarget) return
|
|
314
|
-
|
|
315
|
-
setIsDeleting(true)
|
|
316
|
-
try {
|
|
317
|
-
if (deleteTarget.type === 'asset') {
|
|
318
|
-
await deleteAsset({ id: deleteTarget.id })
|
|
319
|
-
} else {
|
|
320
|
-
await deleteFolder({ id: deleteTarget.id })
|
|
321
|
-
}
|
|
322
|
-
setDeleteTarget(null)
|
|
323
|
-
} catch (err) {
|
|
324
|
-
console.error('Delete failed:', err)
|
|
325
|
-
} finally {
|
|
326
|
-
setIsDeleting(false)
|
|
327
|
-
}
|
|
328
|
-
}, [deleteTarget, deleteAsset, deleteFolder])
|
|
329
|
-
|
|
330
|
-
const handleBulkDelete = useCallback(async () => {
|
|
331
|
-
if (selectedAssets.size === 0) return
|
|
332
|
-
|
|
333
|
-
setIsBulkDeleting(true)
|
|
334
|
-
try {
|
|
335
|
-
const deletePromises = Array.from(selectedAssets).map((id) =>
|
|
336
|
-
deleteAsset({ id })
|
|
337
|
-
)
|
|
338
|
-
await Promise.all(deletePromises)
|
|
339
|
-
setSelectedAssets(new Set())
|
|
340
|
-
setIsSelectionMode(false)
|
|
341
|
-
setShowBulkDeleteConfirm(false)
|
|
342
|
-
} catch (err) {
|
|
343
|
-
console.error('Bulk delete failed:', err)
|
|
344
|
-
} finally {
|
|
345
|
-
setIsBulkDeleting(false)
|
|
346
|
-
}
|
|
347
|
-
}, [selectedAssets, deleteAsset])
|
|
348
|
-
|
|
349
|
-
const handleBulkMoveComplete = useCallback(() => {
|
|
350
|
-
setSelectedAssets(new Set())
|
|
351
|
-
setIsSelectionMode(false)
|
|
352
|
-
}, [])
|
|
353
|
-
|
|
354
|
-
const handleRestore = useCallback(
|
|
355
|
-
async (assetId: string) => {
|
|
356
|
-
setIsRestoring(true)
|
|
357
|
-
try {
|
|
358
|
-
await restoreAsset({ id: assetId })
|
|
359
|
-
} catch (err) {
|
|
360
|
-
console.error('Restore failed:', err)
|
|
361
|
-
} finally {
|
|
362
|
-
setIsRestoring(false)
|
|
363
|
-
}
|
|
364
|
-
},
|
|
365
|
-
[restoreAsset]
|
|
366
|
-
)
|
|
367
|
-
|
|
368
|
-
const handleBulkRestore = useCallback(async () => {
|
|
369
|
-
if (selectedAssets.size === 0) return
|
|
370
|
-
|
|
371
|
-
setIsRestoring(true)
|
|
372
|
-
try {
|
|
373
|
-
const restorePromises = Array.from(selectedAssets).map((id) =>
|
|
374
|
-
restoreAsset({ id })
|
|
375
|
-
)
|
|
376
|
-
await Promise.all(restorePromises)
|
|
377
|
-
setSelectedAssets(new Set())
|
|
378
|
-
setIsSelectionMode(false)
|
|
379
|
-
} catch (err) {
|
|
380
|
-
console.error('Bulk restore failed:', err)
|
|
381
|
-
} finally {
|
|
382
|
-
setIsRestoring(false)
|
|
383
|
-
}
|
|
384
|
-
}, [selectedAssets, restoreAsset])
|
|
385
|
-
|
|
386
|
-
const handleRestoreFolder = useCallback(
|
|
387
|
-
async (folderId: string) => {
|
|
388
|
-
setIsRestoring(true)
|
|
389
|
-
try {
|
|
390
|
-
await restoreFolder({ id: folderId })
|
|
391
|
-
} catch (err) {
|
|
392
|
-
console.error('Restore folder failed:', err)
|
|
393
|
-
} finally {
|
|
394
|
-
setIsRestoring(false)
|
|
395
|
-
}
|
|
396
|
-
},
|
|
397
|
-
[restoreFolder]
|
|
398
|
-
)
|
|
399
|
-
|
|
400
|
-
const handlePermanentDelete = useCallback(async () => {
|
|
401
|
-
if (!permanentDeleteTarget) return
|
|
402
|
-
|
|
403
|
-
setIsPermanentlyDeleting(true)
|
|
404
|
-
try {
|
|
405
|
-
if (permanentDeleteTarget === 'bulk') {
|
|
406
|
-
await bulkPermanentDeleteAssets({ ids: Array.from(selectedAssets) })
|
|
407
|
-
setSelectedAssets(new Set())
|
|
408
|
-
setIsSelectionMode(false)
|
|
409
|
-
} else {
|
|
410
|
-
await permanentDeleteAsset({ id: permanentDeleteTarget })
|
|
411
|
-
}
|
|
412
|
-
setShowPermanentDeleteConfirm(false)
|
|
413
|
-
setPermanentDeleteTarget(null)
|
|
414
|
-
} catch (err) {
|
|
415
|
-
console.error('Permanent delete failed:', err)
|
|
416
|
-
} finally {
|
|
417
|
-
setIsPermanentlyDeleting(false)
|
|
418
|
-
}
|
|
419
|
-
}, [permanentDeleteTarget, permanentDeleteAsset, bulkPermanentDeleteAssets, selectedAssets])
|
|
420
|
-
|
|
421
|
-
const handleCreateFolder = useCallback(async () => {
|
|
422
|
-
if (!newFolderName.trim()) {
|
|
423
|
-
setFolderError('Folder name is required')
|
|
424
|
-
return
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
setIsCreatingFolder(true)
|
|
428
|
-
setFolderError('')
|
|
429
|
-
|
|
430
|
-
try {
|
|
431
|
-
await createFolder({
|
|
432
|
-
name: newFolderName.trim(),
|
|
433
|
-
parentId: currentFolderId,
|
|
434
|
-
})
|
|
435
|
-
setShowNewFolderModal(false)
|
|
436
|
-
setNewFolderName('')
|
|
437
|
-
} catch (error) {
|
|
438
|
-
setFolderError(
|
|
439
|
-
error instanceof Error ? error.message : 'Failed to create folder'
|
|
440
|
-
)
|
|
441
|
-
} finally {
|
|
442
|
-
setIsCreatingFolder(false)
|
|
443
|
-
}
|
|
444
|
-
}, [newFolderName, currentFolderId, createFolder])
|
|
445
|
-
|
|
446
|
-
const handleUploadComplete = useCallback((_results: UploadedFile[]) => {
|
|
447
|
-
setShowUploadModal(false)
|
|
448
|
-
}, [])
|
|
449
|
-
|
|
450
|
-
const isLoading = assetsResult === undefined || folders === undefined
|
|
451
|
-
|
|
452
|
-
const displayedAssets = useMemo(() => {
|
|
453
|
-
const assets = assetsResult?.page ?? []
|
|
454
|
-
if (termFilteredMediaIds === null) {
|
|
455
|
-
return assets
|
|
456
|
-
}
|
|
457
|
-
return assets.filter((asset) => termFilteredMediaIds.has(asset._id))
|
|
458
|
-
}, [assetsResult?.page, termFilteredMediaIds])
|
|
459
|
-
|
|
460
|
-
return (
|
|
461
|
-
<div className="space-y-6 p-6">
|
|
462
|
-
<CmsPageHeader
|
|
463
|
-
title="Media Library"
|
|
464
|
-
description="Upload, organize, and manage media assets for your content."
|
|
465
|
-
/>
|
|
466
|
-
|
|
467
|
-
<Tabs value={activeView} onValueChange={(v) => handleViewChange(v as MediaView)}>
|
|
468
|
-
<TabsList>
|
|
469
|
-
<TabsTrigger value="library">
|
|
470
|
-
<Image className="mr-1.5 size-4" />
|
|
471
|
-
Library
|
|
472
|
-
</TabsTrigger>
|
|
473
|
-
<TabsTrigger value="trash">
|
|
474
|
-
<Trash2 className="mr-1.5 size-4" />
|
|
475
|
-
Trash
|
|
476
|
-
{trashCount && trashCount.total > 0 && (
|
|
477
|
-
<Badge variant="secondary" className="ml-1.5">
|
|
478
|
-
{trashCount.total}
|
|
479
|
-
</Badge>
|
|
480
|
-
)}
|
|
481
|
-
</TabsTrigger>
|
|
482
|
-
</TabsList>
|
|
483
|
-
</Tabs>
|
|
484
|
-
|
|
485
|
-
{!isTrashView && (
|
|
486
|
-
<nav className="flex items-center gap-1" aria-label="Folder navigation">
|
|
487
|
-
<button
|
|
488
|
-
className={cn(
|
|
489
|
-
'flex items-center gap-1.5 rounded-md px-2 py-1 text-sm transition-colors',
|
|
490
|
-
!currentFolderId
|
|
491
|
-
? 'bg-primary/10 font-medium text-primary'
|
|
492
|
-
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
493
|
-
)}
|
|
494
|
-
onClick={handleNavigateToRoot}
|
|
495
|
-
>
|
|
496
|
-
<Home className="size-4" />
|
|
497
|
-
<span>All Files</span>
|
|
498
|
-
</button>
|
|
499
|
-
{breadcrumbPath.map((folder, index) => (
|
|
500
|
-
<span key={folder._id} className="flex items-center">
|
|
501
|
-
<span className="mx-1 text-muted-foreground">/</span>
|
|
502
|
-
<button
|
|
503
|
-
className={cn(
|
|
504
|
-
'rounded-md px-2 py-1 text-sm transition-colors',
|
|
505
|
-
index === breadcrumbPath.length - 1
|
|
506
|
-
? 'bg-primary/10 font-medium text-primary'
|
|
507
|
-
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
508
|
-
)}
|
|
509
|
-
onClick={() => handleFolderClick(folder._id as string)}
|
|
510
|
-
>
|
|
511
|
-
{folder.name}
|
|
512
|
-
</button>
|
|
513
|
-
</span>
|
|
514
|
-
))}
|
|
515
|
-
</nav>
|
|
516
|
-
)}
|
|
517
|
-
|
|
518
|
-
<CmsToolbar
|
|
519
|
-
left={
|
|
520
|
-
<div className="flex items-center gap-3">
|
|
521
|
-
<div className="relative">
|
|
522
|
-
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
|
523
|
-
<Input
|
|
524
|
-
type="search"
|
|
525
|
-
placeholder="Search files..."
|
|
526
|
-
value={searchQuery}
|
|
527
|
-
onChange={(e) => setSearchQuery(e.target.value)}
|
|
528
|
-
className="w-64 pl-9"
|
|
529
|
-
/>
|
|
530
|
-
{searchQuery && (
|
|
531
|
-
<button
|
|
532
|
-
className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 hover:bg-muted"
|
|
533
|
-
onClick={() => setSearchQuery('')}
|
|
534
|
-
aria-label="Clear search"
|
|
535
|
-
>
|
|
536
|
-
<X className="size-4 text-muted-foreground" />
|
|
537
|
-
</button>
|
|
538
|
-
)}
|
|
539
|
-
</div>
|
|
540
|
-
|
|
541
|
-
<Select
|
|
542
|
-
value={typeFilter || 'all'}
|
|
543
|
-
onValueChange={(v) => setTypeFilter(v === 'all' ? '' : (v as MediaType))}
|
|
544
|
-
>
|
|
545
|
-
<SelectTrigger className="w-36">
|
|
546
|
-
<SelectValue placeholder="All Types" />
|
|
547
|
-
</SelectTrigger>
|
|
548
|
-
<SelectContent>
|
|
549
|
-
<SelectItem value="all">All Types</SelectItem>
|
|
550
|
-
<SelectItem value="image">Images</SelectItem>
|
|
551
|
-
<SelectItem value="video">Videos</SelectItem>
|
|
552
|
-
<SelectItem value="audio">Audio</SelectItem>
|
|
553
|
-
<SelectItem value="document">Documents</SelectItem>
|
|
554
|
-
<SelectItem value="other">Other</SelectItem>
|
|
555
|
-
</SelectContent>
|
|
556
|
-
</Select>
|
|
557
|
-
|
|
558
|
-
<TaxonomyFilter
|
|
559
|
-
selectedTermIds={selectedTermIds}
|
|
560
|
-
onChange={setSelectedTermIds}
|
|
561
|
-
placeholder="Tags"
|
|
562
|
-
/>
|
|
563
|
-
|
|
564
|
-
{assetsResult?.page && assetsResult.page.length > 0 && (
|
|
565
|
-
<label className="flex cursor-pointer items-center gap-2 text-sm">
|
|
566
|
-
<Checkbox
|
|
567
|
-
checked={isSelectionMode}
|
|
568
|
-
onCheckedChange={(checked) => {
|
|
569
|
-
setIsSelectionMode(checked as boolean)
|
|
570
|
-
if (!checked) {
|
|
571
|
-
setSelectedAssets(new Set())
|
|
572
|
-
}
|
|
573
|
-
}}
|
|
574
|
-
/>
|
|
575
|
-
Selection Mode
|
|
576
|
-
</label>
|
|
577
|
-
)}
|
|
578
|
-
</div>
|
|
579
|
-
}
|
|
580
|
-
right={
|
|
581
|
-
<div className="flex items-center gap-2">
|
|
582
|
-
{isSelectionMode && selectedAssets.size > 0 && (
|
|
583
|
-
<span className="text-sm text-muted-foreground">
|
|
584
|
-
{selectedAssets.size} selected
|
|
585
|
-
</span>
|
|
586
|
-
)}
|
|
587
|
-
|
|
588
|
-
{isSelectionMode && (
|
|
589
|
-
<>
|
|
590
|
-
<CmsButton variant="secondary" size="sm" onClick={handleSelectAll}>
|
|
591
|
-
Select All
|
|
592
|
-
</CmsButton>
|
|
593
|
-
<CmsButton variant="secondary" size="sm" onClick={handleDeselectAll}>
|
|
594
|
-
Clear
|
|
595
|
-
</CmsButton>
|
|
596
|
-
</>
|
|
597
|
-
)}
|
|
598
|
-
|
|
599
|
-
{currentFolderId && !isTrashView && (
|
|
600
|
-
<CmsButton variant="secondary" onClick={handleNavigateUp}>
|
|
601
|
-
<ChevronLeft className="size-4" />
|
|
602
|
-
Up
|
|
603
|
-
</CmsButton>
|
|
604
|
-
)}
|
|
605
|
-
|
|
606
|
-
{!isTrashView && (
|
|
607
|
-
<>
|
|
608
|
-
<CmsButton
|
|
609
|
-
variant="secondary"
|
|
610
|
-
onClick={() => setShowNewFolderModal(true)}
|
|
611
|
-
>
|
|
612
|
-
<FolderPlus className="size-4" />
|
|
613
|
-
New Folder
|
|
614
|
-
</CmsButton>
|
|
615
|
-
|
|
616
|
-
<CmsButton onClick={() => setShowUploadModal(true)}>
|
|
617
|
-
<Upload className="size-4" />
|
|
618
|
-
Upload Files
|
|
619
|
-
</CmsButton>
|
|
620
|
-
</>
|
|
621
|
-
)}
|
|
622
|
-
</div>
|
|
623
|
-
}
|
|
624
|
-
/>
|
|
625
|
-
|
|
626
|
-
{isLoading ? (
|
|
627
|
-
<div className="flex flex-col items-center justify-center py-12">
|
|
628
|
-
<div className="size-8 animate-spin rounded-full border-2 border-muted border-t-primary" />
|
|
629
|
-
<p className="mt-4 text-sm text-muted-foreground">
|
|
630
|
-
Loading media library...
|
|
631
|
-
</p>
|
|
632
|
-
</div>
|
|
633
|
-
) : (
|
|
634
|
-
<>
|
|
635
|
-
{!isTrashView && folders && folders.length > 0 && !searchQuery && (
|
|
636
|
-
<section>
|
|
637
|
-
<h3 className="mb-3 text-sm font-medium text-muted-foreground">
|
|
638
|
-
Folders
|
|
639
|
-
</h3>
|
|
640
|
-
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4 md:grid-cols-6">
|
|
641
|
-
{folders.map((folder) => (
|
|
642
|
-
<div
|
|
643
|
-
key={folder._id}
|
|
644
|
-
className="group relative flex flex-col items-center gap-2 rounded-lg border bg-card p-4 text-center transition-colors hover:border-primary/50 hover:bg-muted/50 cursor-pointer"
|
|
645
|
-
onClick={() =>
|
|
646
|
-
handleFolderClick(folder._id as string)
|
|
647
|
-
}
|
|
648
|
-
>
|
|
649
|
-
<div className="absolute right-2 top-2">
|
|
650
|
-
<MediaFolderActions
|
|
651
|
-
folder={{ _id: folder._id, name: folder.name }}
|
|
652
|
-
onEdit={() =>
|
|
653
|
-
setEditingFolder({
|
|
654
|
-
_id: folder._id,
|
|
655
|
-
name: folder.name,
|
|
656
|
-
description: folder.description,
|
|
657
|
-
})
|
|
658
|
-
}
|
|
659
|
-
onDelete={() =>
|
|
660
|
-
setDeleteTarget({
|
|
661
|
-
type: 'folder',
|
|
662
|
-
id: folder._id,
|
|
663
|
-
name: folder.name,
|
|
664
|
-
})
|
|
665
|
-
}
|
|
666
|
-
/>
|
|
667
|
-
</div>
|
|
668
|
-
<Folder className="size-10 text-amber-500" />
|
|
669
|
-
<span className="truncate text-sm font-medium">
|
|
670
|
-
{folder.name}
|
|
671
|
-
</span>
|
|
672
|
-
</div>
|
|
673
|
-
))}
|
|
674
|
-
</div>
|
|
675
|
-
</section>
|
|
676
|
-
)}
|
|
677
|
-
|
|
678
|
-
{isTrashView && folders && folders.length > 0 && (
|
|
679
|
-
<section>
|
|
680
|
-
<h3 className="mb-3 text-sm font-medium text-muted-foreground">
|
|
681
|
-
Deleted Folders ({folders.length})
|
|
682
|
-
</h3>
|
|
683
|
-
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4 md:grid-cols-6">
|
|
684
|
-
{folders.map((folder) => (
|
|
685
|
-
<div
|
|
686
|
-
key={folder._id}
|
|
687
|
-
className="group relative flex flex-col items-center gap-2 rounded-lg border border-destructive/20 bg-card p-4 text-center opacity-60"
|
|
688
|
-
>
|
|
689
|
-
<Folder className="size-10 text-amber-500/50" />
|
|
690
|
-
<span className="truncate text-sm font-medium">
|
|
691
|
-
{folder.name}
|
|
692
|
-
</span>
|
|
693
|
-
<CmsButton
|
|
694
|
-
variant="secondary"
|
|
695
|
-
size="sm"
|
|
696
|
-
onClick={() => handleRestoreFolder(folder._id)}
|
|
697
|
-
disabled={isRestoring}
|
|
698
|
-
>
|
|
699
|
-
<RotateCcw className="mr-1 size-3" />
|
|
700
|
-
Restore
|
|
701
|
-
</CmsButton>
|
|
702
|
-
</div>
|
|
703
|
-
))}
|
|
704
|
-
</div>
|
|
705
|
-
</section>
|
|
706
|
-
)}
|
|
707
|
-
|
|
708
|
-
{displayedAssets.length > 0 ? (
|
|
709
|
-
<section>
|
|
710
|
-
{!isTrashView && folders && folders.length > 0 && !searchQuery && (
|
|
711
|
-
<h3 className="mb-3 text-sm font-medium text-muted-foreground">
|
|
712
|
-
Files
|
|
713
|
-
</h3>
|
|
714
|
-
)}
|
|
715
|
-
{isTrashView && (
|
|
716
|
-
<h3 className="mb-3 text-sm font-medium text-muted-foreground">
|
|
717
|
-
Deleted Files ({displayedAssets.length})
|
|
718
|
-
</h3>
|
|
719
|
-
)}
|
|
720
|
-
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4 md:grid-cols-6">
|
|
721
|
-
{displayedAssets.map((asset) => {
|
|
722
|
-
const assetId = asset._id as string
|
|
723
|
-
const isSelected = selectedAssets.has(assetId)
|
|
724
|
-
const mediaType = getMediaTypeFromMimeType(asset.mimeType)
|
|
725
|
-
|
|
726
|
-
return (
|
|
727
|
-
<div
|
|
728
|
-
key={asset._id}
|
|
729
|
-
className={cn(
|
|
730
|
-
'group relative flex flex-col overflow-hidden rounded-lg border bg-card transition-all cursor-pointer hover:border-primary/50',
|
|
731
|
-
isSelected && 'border-primary ring-2 ring-primary/20'
|
|
732
|
-
)}
|
|
733
|
-
onClick={() => handleAssetClick(assetId)}
|
|
734
|
-
>
|
|
735
|
-
{isSelectionMode && (
|
|
736
|
-
<div className="absolute left-2 top-2 z-10">
|
|
737
|
-
<Checkbox
|
|
738
|
-
checked={isSelected}
|
|
739
|
-
onCheckedChange={() => handleAssetSelect(assetId)}
|
|
740
|
-
onClick={(e) => e.stopPropagation()}
|
|
741
|
-
className="bg-white/80"
|
|
742
|
-
/>
|
|
743
|
-
</div>
|
|
744
|
-
)}
|
|
745
|
-
|
|
746
|
-
{!isSelectionMode && !isTrashView && (
|
|
747
|
-
<div className="absolute right-2 top-2 z-10">
|
|
748
|
-
<MediaAssetActions
|
|
749
|
-
asset={{
|
|
750
|
-
_id: asset._id,
|
|
751
|
-
name: asset.name,
|
|
752
|
-
url: asset.url,
|
|
753
|
-
}}
|
|
754
|
-
onView={() => {
|
|
755
|
-
const index = displayedAssets.findIndex(
|
|
756
|
-
(a) => a._id === asset._id
|
|
757
|
-
)
|
|
758
|
-
if (index !== -1) setPreviewIndex(index)
|
|
759
|
-
}}
|
|
760
|
-
onEdit={() =>
|
|
761
|
-
setEditingAsset({
|
|
762
|
-
_id: asset._id,
|
|
763
|
-
name: asset.name,
|
|
764
|
-
title: asset.title,
|
|
765
|
-
description: asset.description,
|
|
766
|
-
altText: asset.altText,
|
|
767
|
-
tags: asset.tags,
|
|
768
|
-
})
|
|
769
|
-
}
|
|
770
|
-
onDelete={() =>
|
|
771
|
-
setDeleteTarget({
|
|
772
|
-
type: 'asset',
|
|
773
|
-
id: asset._id,
|
|
774
|
-
name: asset.name,
|
|
775
|
-
})
|
|
776
|
-
}
|
|
777
|
-
/>
|
|
778
|
-
</div>
|
|
779
|
-
)}
|
|
780
|
-
|
|
781
|
-
{!isSelectionMode && isTrashView && (
|
|
782
|
-
<div className="absolute right-2 top-2 z-10 flex gap-1 opacity-0 group-hover:opacity-100">
|
|
783
|
-
<CmsButton
|
|
784
|
-
variant="secondary"
|
|
785
|
-
size="icon-sm"
|
|
786
|
-
onClick={(e) => {
|
|
787
|
-
e.stopPropagation()
|
|
788
|
-
handleRestore(asset._id)
|
|
789
|
-
}}
|
|
790
|
-
title="Restore"
|
|
791
|
-
>
|
|
792
|
-
<RotateCcw className="size-4" />
|
|
793
|
-
</CmsButton>
|
|
794
|
-
<CmsButton
|
|
795
|
-
variant="danger"
|
|
796
|
-
size="icon-sm"
|
|
797
|
-
onClick={(e) => {
|
|
798
|
-
e.stopPropagation()
|
|
799
|
-
setPermanentDeleteTarget(asset._id)
|
|
800
|
-
setShowPermanentDeleteConfirm(true)
|
|
801
|
-
}}
|
|
802
|
-
title="Delete Forever"
|
|
803
|
-
>
|
|
804
|
-
<Trash2 className="size-4" />
|
|
805
|
-
</CmsButton>
|
|
806
|
-
</div>
|
|
807
|
-
)}
|
|
808
|
-
|
|
809
|
-
<div className="aspect-square overflow-hidden bg-muted">
|
|
810
|
-
{mediaType === 'image' && asset.url ? (
|
|
811
|
-
<img
|
|
812
|
-
src={asset.url}
|
|
813
|
-
alt={asset.title || asset.name}
|
|
814
|
-
className="h-full w-full object-cover"
|
|
815
|
-
/>
|
|
816
|
-
) : (
|
|
817
|
-
<div className="flex size-full items-center justify-center text-muted-foreground">
|
|
818
|
-
{getMediaTypeIcon(mediaType, 'size-10')}
|
|
819
|
-
</div>
|
|
820
|
-
)}
|
|
821
|
-
</div>
|
|
822
|
-
|
|
823
|
-
<div className="p-2">
|
|
824
|
-
<p
|
|
825
|
-
className="truncate text-sm font-medium"
|
|
826
|
-
title={asset.name}
|
|
827
|
-
>
|
|
828
|
-
{asset.name}
|
|
829
|
-
</p>
|
|
830
|
-
<div className="mt-0.5 flex items-center gap-1 text-xs text-muted-foreground">
|
|
831
|
-
<span className="capitalize">{mediaType}</span>
|
|
832
|
-
<span>•</span>
|
|
833
|
-
<span>{formatFileSize(asset.size ?? 0)}</span>
|
|
834
|
-
</div>
|
|
835
|
-
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
836
|
-
{formatDate(asset._creationTime)}
|
|
837
|
-
</p>
|
|
838
|
-
</div>
|
|
839
|
-
</div>
|
|
840
|
-
)
|
|
841
|
-
})}
|
|
842
|
-
</div>
|
|
843
|
-
|
|
844
|
-
{!assetsResult.isDone && (
|
|
845
|
-
<p className="mt-4 text-center text-sm text-muted-foreground">
|
|
846
|
-
Showing {assetsResult.page.length} files. More files available.
|
|
847
|
-
</p>
|
|
848
|
-
)}
|
|
849
|
-
</section>
|
|
850
|
-
) : isTrashView ? (
|
|
851
|
-
<CmsEmptyState
|
|
852
|
-
icon={<Trash2 className="size-8" />}
|
|
853
|
-
title="Trash is empty"
|
|
854
|
-
description="Deleted files will appear here. You can restore them or permanently delete them."
|
|
855
|
-
/>
|
|
856
|
-
) : (
|
|
857
|
-
!folders?.length && (
|
|
858
|
-
<CmsEmptyState
|
|
859
|
-
icon={<Image className="size-8" />}
|
|
860
|
-
title="No media assets yet"
|
|
861
|
-
description="Upload images, videos, documents, and other files to use in your content."
|
|
862
|
-
action={{
|
|
863
|
-
label: 'Upload Files',
|
|
864
|
-
onClick: () => setShowUploadModal(true),
|
|
865
|
-
}}
|
|
866
|
-
/>
|
|
867
|
-
)
|
|
868
|
-
)}
|
|
869
|
-
|
|
870
|
-
{searchQuery && displayedAssets.length === 0 && !isTrashView && (
|
|
871
|
-
<CmsEmptyState
|
|
872
|
-
icon={<Search className="size-8" />}
|
|
873
|
-
title="No results found"
|
|
874
|
-
description={`No files match "${searchQuery}". Try a different search term.`}
|
|
875
|
-
action={{
|
|
876
|
-
label: 'Clear Search',
|
|
877
|
-
onClick: () => setSearchQuery(''),
|
|
878
|
-
variant: 'secondary',
|
|
879
|
-
}}
|
|
880
|
-
/>
|
|
881
|
-
)}
|
|
882
|
-
</>
|
|
883
|
-
)}
|
|
884
|
-
|
|
885
|
-
<Dialog open={showNewFolderModal} onOpenChange={setShowNewFolderModal}>
|
|
886
|
-
<DialogContent>
|
|
887
|
-
<DialogHeader>
|
|
888
|
-
<DialogTitle>Create New Folder</DialogTitle>
|
|
889
|
-
</DialogHeader>
|
|
890
|
-
<div className="space-y-4 py-4">
|
|
891
|
-
<div className="space-y-2">
|
|
892
|
-
<Label htmlFor="folder-name">Folder Name</Label>
|
|
893
|
-
<Input
|
|
894
|
-
id="folder-name"
|
|
895
|
-
value={newFolderName}
|
|
896
|
-
onChange={(e) => {
|
|
897
|
-
setNewFolderName(e.target.value)
|
|
898
|
-
setFolderError('')
|
|
899
|
-
}}
|
|
900
|
-
placeholder="Enter folder name"
|
|
901
|
-
autoFocus
|
|
902
|
-
onKeyDown={(e) => {
|
|
903
|
-
if (e.key === 'Enter' && !isCreatingFolder) {
|
|
904
|
-
handleCreateFolder()
|
|
905
|
-
}
|
|
906
|
-
}}
|
|
907
|
-
/>
|
|
908
|
-
{folderError && (
|
|
909
|
-
<p className="text-sm text-destructive">{folderError}</p>
|
|
910
|
-
)}
|
|
911
|
-
</div>
|
|
912
|
-
</div>
|
|
913
|
-
<DialogFooter>
|
|
914
|
-
<CmsButton
|
|
915
|
-
variant="secondary"
|
|
916
|
-
onClick={() => setShowNewFolderModal(false)}
|
|
917
|
-
disabled={isCreatingFolder}
|
|
918
|
-
>
|
|
919
|
-
Cancel
|
|
920
|
-
</CmsButton>
|
|
921
|
-
<CmsButton
|
|
922
|
-
onClick={handleCreateFolder}
|
|
923
|
-
disabled={isCreatingFolder || !newFolderName.trim()}
|
|
924
|
-
loading={isCreatingFolder}
|
|
925
|
-
>
|
|
926
|
-
Create Folder
|
|
927
|
-
</CmsButton>
|
|
928
|
-
</DialogFooter>
|
|
929
|
-
</DialogContent>
|
|
930
|
-
</Dialog>
|
|
931
|
-
|
|
932
|
-
<Dialog open={showUploadModal} onOpenChange={setShowUploadModal}>
|
|
933
|
-
<DialogContent className="max-w-lg">
|
|
934
|
-
<DialogHeader>
|
|
935
|
-
<DialogTitle>Upload Files</DialogTitle>
|
|
936
|
-
</DialogHeader>
|
|
937
|
-
<div className="py-4">
|
|
938
|
-
<UploadDropzone
|
|
939
|
-
currentFolderId={currentFolderId}
|
|
940
|
-
generateUploadUrl={api.media.generateUploadUrl}
|
|
941
|
-
createAsset={api.media.createAsset}
|
|
942
|
-
onUploadComplete={handleUploadComplete}
|
|
943
|
-
maxFileSize={50 * 1024 * 1024}
|
|
944
|
-
maxConcurrentUploads={3}
|
|
945
|
-
/>
|
|
946
|
-
</div>
|
|
947
|
-
<DialogFooter>
|
|
948
|
-
<CmsButton variant="secondary" onClick={() => setShowUploadModal(false)}>
|
|
949
|
-
Close
|
|
950
|
-
</CmsButton>
|
|
951
|
-
</DialogFooter>
|
|
952
|
-
</DialogContent>
|
|
953
|
-
</Dialog>
|
|
954
|
-
|
|
955
|
-
{/* Preview Modal */}
|
|
956
|
-
<MediaPreviewModal
|
|
957
|
-
asset={
|
|
958
|
-
previewIndex !== null && displayedAssets[previewIndex]
|
|
959
|
-
? (displayedAssets[previewIndex] as MediaAsset)
|
|
960
|
-
: null
|
|
961
|
-
}
|
|
962
|
-
assets={displayedAssets as MediaAsset[]}
|
|
963
|
-
currentIndex={previewIndex ?? 0}
|
|
964
|
-
open={previewIndex !== null}
|
|
965
|
-
onOpenChange={(open) => {
|
|
966
|
-
if (!open) setPreviewIndex(null)
|
|
967
|
-
}}
|
|
968
|
-
onNavigate={handlePreviewNavigate}
|
|
969
|
-
onEdit={
|
|
970
|
-
isTrashView
|
|
971
|
-
? undefined
|
|
972
|
-
: (asset) =>
|
|
973
|
-
setEditingAsset({
|
|
974
|
-
_id: asset._id,
|
|
975
|
-
name: asset.name,
|
|
976
|
-
title: asset.title,
|
|
977
|
-
description: asset.description,
|
|
978
|
-
altText: asset.altText,
|
|
979
|
-
tags: asset.tags,
|
|
980
|
-
})
|
|
981
|
-
}
|
|
982
|
-
onDelete={
|
|
983
|
-
isTrashView
|
|
984
|
-
? undefined
|
|
985
|
-
: (asset) =>
|
|
986
|
-
setDeleteTarget({
|
|
987
|
-
type: 'asset',
|
|
988
|
-
id: asset._id,
|
|
989
|
-
name: asset.name,
|
|
990
|
-
})
|
|
991
|
-
}
|
|
992
|
-
/>
|
|
993
|
-
|
|
994
|
-
{/* Edit Asset Dialog */}
|
|
995
|
-
<MediaAssetEditDialog
|
|
996
|
-
asset={editingAsset}
|
|
997
|
-
open={editingAsset !== null}
|
|
998
|
-
onOpenChange={(open) => {
|
|
999
|
-
if (!open) setEditingAsset(null)
|
|
1000
|
-
}}
|
|
1001
|
-
/>
|
|
1002
|
-
|
|
1003
|
-
{/* Edit Folder Dialog */}
|
|
1004
|
-
<MediaFolderEditDialog
|
|
1005
|
-
folder={editingFolder}
|
|
1006
|
-
open={editingFolder !== null}
|
|
1007
|
-
onOpenChange={(open) => {
|
|
1008
|
-
if (!open) setEditingFolder(null)
|
|
1009
|
-
}}
|
|
1010
|
-
/>
|
|
1011
|
-
|
|
1012
|
-
{/* Delete Confirmation Dialog */}
|
|
1013
|
-
<CmsConfirmDialog
|
|
1014
|
-
open={deleteTarget !== null}
|
|
1015
|
-
onOpenChange={(open) => {
|
|
1016
|
-
if (!open) setDeleteTarget(null)
|
|
1017
|
-
}}
|
|
1018
|
-
title={`Delete ${deleteTarget?.type === 'folder' ? 'Folder' : 'File'}?`}
|
|
1019
|
-
description={`Are you sure you want to delete "${deleteTarget?.name}"? ${deleteTarget?.type === 'folder' ? 'This will also delete all files inside the folder.' : 'This action can be undone from the trash.'}`}
|
|
1020
|
-
confirmLabel="Delete"
|
|
1021
|
-
onConfirm={handleDelete}
|
|
1022
|
-
variant="danger"
|
|
1023
|
-
loading={isDeleting}
|
|
1024
|
-
/>
|
|
1025
|
-
|
|
1026
|
-
{/* Bulk Delete Confirmation Dialog */}
|
|
1027
|
-
<CmsConfirmDialog
|
|
1028
|
-
open={showBulkDeleteConfirm}
|
|
1029
|
-
onOpenChange={setShowBulkDeleteConfirm}
|
|
1030
|
-
title="Delete Selected Files?"
|
|
1031
|
-
description={`Are you sure you want to delete ${selectedAssets.size} ${selectedAssets.size === 1 ? 'file' : 'files'}? This action can be undone from the trash.`}
|
|
1032
|
-
confirmLabel="Delete All"
|
|
1033
|
-
onConfirm={handleBulkDelete}
|
|
1034
|
-
variant="danger"
|
|
1035
|
-
loading={isBulkDeleting}
|
|
1036
|
-
/>
|
|
1037
|
-
|
|
1038
|
-
{/* Permanent Delete Confirmation Dialog */}
|
|
1039
|
-
<CmsConfirmDialog
|
|
1040
|
-
open={showPermanentDeleteConfirm}
|
|
1041
|
-
onOpenChange={(open) => {
|
|
1042
|
-
setShowPermanentDeleteConfirm(open)
|
|
1043
|
-
if (!open) setPermanentDeleteTarget(null)
|
|
1044
|
-
}}
|
|
1045
|
-
title={permanentDeleteTarget === 'bulk'
|
|
1046
|
-
? `Delete ${selectedAssets.size} ${selectedAssets.size === 1 ? 'File' : 'Files'} Forever?`
|
|
1047
|
-
: 'Delete Forever?'
|
|
1048
|
-
}
|
|
1049
|
-
description={permanentDeleteTarget === 'bulk'
|
|
1050
|
-
? `This will permanently delete ${selectedAssets.size} ${selectedAssets.size === 1 ? 'file' : 'files'}. This action cannot be undone.`
|
|
1051
|
-
: 'This will permanently delete this file. This action cannot be undone.'
|
|
1052
|
-
}
|
|
1053
|
-
confirmLabel="Delete Forever"
|
|
1054
|
-
onConfirm={handlePermanentDelete}
|
|
1055
|
-
variant="danger"
|
|
1056
|
-
loading={isPermanentlyDeleting}
|
|
1057
|
-
/>
|
|
1058
|
-
|
|
1059
|
-
{/* Move Modal */}
|
|
1060
|
-
<MediaMoveModal
|
|
1061
|
-
open={showMoveModal}
|
|
1062
|
-
onOpenChange={setShowMoveModal}
|
|
1063
|
-
assetIds={Array.from(selectedAssets)}
|
|
1064
|
-
currentFolderId={currentFolderId}
|
|
1065
|
-
onMoved={handleBulkMoveComplete}
|
|
1066
|
-
/>
|
|
1067
|
-
|
|
1068
|
-
{/* Bulk Action Bar - Library View */}
|
|
1069
|
-
{isSelectionMode && !isTrashView && (
|
|
1070
|
-
<MediaBulkActionBar
|
|
1071
|
-
selectedCount={selectedAssets.size}
|
|
1072
|
-
onClear={handleDeselectAll}
|
|
1073
|
-
onMove={() => setShowMoveModal(true)}
|
|
1074
|
-
onDelete={() => setShowBulkDeleteConfirm(true)}
|
|
1075
|
-
isDeleting={isBulkDeleting}
|
|
1076
|
-
/>
|
|
1077
|
-
)}
|
|
1078
|
-
|
|
1079
|
-
{/* Bulk Action Bar - Trash View */}
|
|
1080
|
-
{isSelectionMode && isTrashView && (
|
|
1081
|
-
<MediaTrashBulkActionBar
|
|
1082
|
-
selectedCount={selectedAssets.size}
|
|
1083
|
-
onClear={handleDeselectAll}
|
|
1084
|
-
onRestore={handleBulkRestore}
|
|
1085
|
-
onPermanentDelete={() => {
|
|
1086
|
-
setPermanentDeleteTarget('bulk')
|
|
1087
|
-
setShowPermanentDeleteConfirm(true)
|
|
1088
|
-
}}
|
|
1089
|
-
isRestoring={isRestoring}
|
|
1090
|
-
isDeleting={isPermanentlyDeleting}
|
|
1091
|
-
/>
|
|
1092
|
-
)}
|
|
1093
|
-
</div>
|
|
1094
|
-
)
|
|
1
|
+
/**
|
|
2
|
+
* Media Route (CLI)
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper around the shared MediaPage component.
|
|
5
|
+
* Provides TanStack Router integration and API access.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
9
|
+
import { api } from "../../convex/_generated/api";
|
|
10
|
+
import { MediaPage } from "~/pages";
|
|
11
|
+
import { useTanStackNavigation } from "~/lib/tanstack-adapter";
|
|
12
|
+
import { useSettingsConfig } from "~/contexts";
|
|
13
|
+
|
|
14
|
+
export const Route = createFileRoute("/media")({
|
|
15
|
+
component: MediaRoute,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function MediaRoute() {
|
|
19
|
+
const navigation = useTanStackNavigation();
|
|
20
|
+
const { settings } = useSettingsConfig();
|
|
21
|
+
return (
|
|
22
|
+
<MediaPage api={api.admin} navigation={navigation} settings={settings} />
|
|
23
|
+
);
|
|
1095
24
|
}
|