@actuate-media/cms-admin 0.8.0 → 0.8.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/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +44 -42
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/lib/search.test.js +10 -10
- package/dist/__tests__/lib/search.test.js.map +1 -1
- package/dist/__tests__/lib/utils.test.js.map +1 -1
- package/dist/__tests__/router/match-route.test.js.map +1 -1
- package/dist/__tests__/router/strip-base.test.js.map +1 -1
- package/dist/components/Breadcrumbs.d.ts.map +1 -1
- package/dist/components/Breadcrumbs.js +2 -4
- package/dist/components/Breadcrumbs.js.map +1 -1
- package/dist/components/CommandPalette.d.ts.map +1 -1
- package/dist/components/CommandPalette.js +7 -3
- package/dist/components/CommandPalette.js.map +1 -1
- package/dist/components/ContentOverviewChart.d.ts.map +1 -1
- package/dist/components/ContentOverviewChart.js.map +1 -1
- package/dist/components/ErrorBoundary.d.ts.map +1 -1
- package/dist/components/ErrorBoundary.js.map +1 -1
- package/dist/components/FocalPointPicker.d.ts.map +1 -1
- package/dist/components/FocalPointPicker.js +4 -2
- package/dist/components/FocalPointPicker.js.map +1 -1
- package/dist/components/FolderTree.d.ts.map +1 -1
- package/dist/components/FolderTree.js +18 -10
- package/dist/components/FolderTree.js.map +1 -1
- package/dist/components/LivePreview.d.ts +1 -1
- package/dist/components/LivePreview.d.ts.map +1 -1
- package/dist/components/LivePreview.js +6 -2
- package/dist/components/LivePreview.js.map +1 -1
- package/dist/components/LocaleProvider.d.ts.map +1 -1
- package/dist/components/LocaleProvider.js.map +1 -1
- package/dist/components/LocaleSwitcher.d.ts.map +1 -1
- package/dist/components/LocaleSwitcher.js +1 -1
- package/dist/components/LocaleSwitcher.js.map +1 -1
- package/dist/components/MediaPickerModal.d.ts.map +1 -1
- package/dist/components/MediaPickerModal.js.map +1 -1
- package/dist/components/PresenceIndicator.d.ts.map +1 -1
- package/dist/components/PresenceIndicator.js +5 -2
- package/dist/components/PresenceIndicator.js.map +1 -1
- package/dist/components/SEOPanel.d.ts +1 -1
- package/dist/components/SEOPanel.d.ts.map +1 -1
- package/dist/components/SEOPanel.js +110 -24
- package/dist/components/SEOPanel.js.map +1 -1
- package/dist/components/SEOPerformance.d.ts.map +1 -1
- package/dist/components/SEOPerformance.js +2 -2
- package/dist/components/SEOPerformance.js.map +1 -1
- package/dist/components/ThemeProvider.d.ts.map +1 -1
- package/dist/components/ThemeProvider.js.map +1 -1
- package/dist/components/TipTapEditor.d.ts.map +1 -1
- package/dist/components/TipTapEditor.js +5 -1
- package/dist/components/TipTapEditor.js.map +1 -1
- package/dist/components/VersionHistory.d.ts +1 -1
- package/dist/components/VersionHistory.d.ts.map +1 -1
- package/dist/components/VersionHistory.js +1 -1
- package/dist/components/VersionHistory.js.map +1 -1
- package/dist/components/ui/Avatar.d.ts.map +1 -1
- package/dist/components/ui/Avatar.js.map +1 -1
- package/dist/components/ui/Badge.d.ts.map +1 -1
- package/dist/components/ui/Badge.js.map +1 -1
- package/dist/components/ui/Button.d.ts.map +1 -1
- package/dist/components/ui/Button.js.map +1 -1
- package/dist/components/ui/CommandPalette.d.ts.map +1 -1
- package/dist/components/ui/CommandPalette.js +8 -2
- package/dist/components/ui/CommandPalette.js.map +1 -1
- package/dist/components/ui/ConfirmDialog.d.ts.map +1 -1
- package/dist/components/ui/ConfirmDialog.js.map +1 -1
- package/dist/components/ui/DataTable.d.ts.map +1 -1
- package/dist/components/ui/DataTable.js +1 -3
- package/dist/components/ui/DataTable.js.map +1 -1
- package/dist/components/ui/EmptyState.d.ts.map +1 -1
- package/dist/components/ui/EmptyState.js +1 -1
- package/dist/components/ui/EmptyState.js.map +1 -1
- package/dist/components/ui/Modal.d.ts.map +1 -1
- package/dist/components/ui/Modal.js.map +1 -1
- package/dist/components/ui/Pagination.d.ts +1 -1
- package/dist/components/ui/Pagination.d.ts.map +1 -1
- package/dist/components/ui/Pagination.js +7 -2
- package/dist/components/ui/Pagination.js.map +1 -1
- package/dist/components/ui/SearchInput.d.ts.map +1 -1
- package/dist/components/ui/SearchInput.js.map +1 -1
- package/dist/components/ui/Skeleton.d.ts.map +1 -1
- package/dist/components/ui/Skeleton.js.map +1 -1
- package/dist/components/ui/Toast.d.ts.map +1 -1
- package/dist/components/ui/Toast.js.map +1 -1
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js.map +1 -1
- package/dist/fields/ArrayField.d.ts.map +1 -1
- package/dist/fields/ArrayField.js +1 -1
- package/dist/fields/ArrayField.js.map +1 -1
- package/dist/fields/BlockBuilderField.d.ts.map +1 -1
- package/dist/fields/BlockBuilderField.js +7 -7
- package/dist/fields/BlockBuilderField.js.map +1 -1
- package/dist/fields/DateField.d.ts.map +1 -1
- package/dist/fields/DateField.js +1 -1
- package/dist/fields/DateField.js.map +1 -1
- package/dist/fields/FieldRenderer.d.ts.map +1 -1
- package/dist/fields/FieldRenderer.js.map +1 -1
- package/dist/fields/GroupField.d.ts.map +1 -1
- package/dist/fields/GroupField.js +1 -1
- package/dist/fields/GroupField.js.map +1 -1
- package/dist/fields/MediaField.d.ts.map +1 -1
- package/dist/fields/MediaField.js +1 -1
- package/dist/fields/MediaField.js.map +1 -1
- package/dist/fields/NavBuilderField.d.ts.map +1 -1
- package/dist/fields/NavBuilderField.js +2 -5
- package/dist/fields/NavBuilderField.js.map +1 -1
- package/dist/fields/NumberField.d.ts +1 -1
- package/dist/fields/NumberField.d.ts.map +1 -1
- package/dist/fields/NumberField.js +2 -2
- package/dist/fields/NumberField.js.map +1 -1
- package/dist/fields/RelationshipField.d.ts.map +1 -1
- package/dist/fields/RelationshipField.js +7 -3
- package/dist/fields/RelationshipField.js.map +1 -1
- package/dist/fields/RichTextField.d.ts +1 -1
- package/dist/fields/RichTextField.d.ts.map +1 -1
- package/dist/fields/RichTextField.js +2 -2
- package/dist/fields/RichTextField.js.map +1 -1
- package/dist/fields/SelectField.d.ts.map +1 -1
- package/dist/fields/SelectField.js +9 -7
- package/dist/fields/SelectField.js.map +1 -1
- package/dist/fields/SlugField.d.ts.map +1 -1
- package/dist/fields/SlugField.js +1 -1
- package/dist/fields/SlugField.js.map +1 -1
- package/dist/fields/TextField.d.ts +1 -1
- package/dist/fields/TextField.d.ts.map +1 -1
- package/dist/fields/TextField.js +2 -2
- package/dist/fields/TextField.js.map +1 -1
- package/dist/fields/ToggleField.d.ts.map +1 -1
- package/dist/fields/ToggleField.js +1 -1
- package/dist/fields/ToggleField.js.map +1 -1
- package/dist/fields/block-types.d.ts.map +1 -1
- package/dist/fields/block-types.js +28 -8
- package/dist/fields/block-types.js.map +1 -1
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js.map +1 -1
- package/dist/hooks/useBuilderState.d.ts.map +1 -1
- package/dist/hooks/useBuilderState.js.map +1 -1
- package/dist/hooks/useContentLock.d.ts.map +1 -1
- package/dist/hooks/useContentLock.js.map +1 -1
- package/dist/hooks/useDebounce.js.map +1 -1
- package/dist/hooks/useKeyboardShortcuts.d.ts.map +1 -1
- package/dist/hooks/useKeyboardShortcuts.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/layout/Header.d.ts.map +1 -1
- package/dist/layout/Header.js.map +1 -1
- package/dist/layout/Layout.d.ts.map +1 -1
- package/dist/layout/Layout.js.map +1 -1
- package/dist/layout/Sidebar.d.ts +1 -1
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +5 -8
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/lib/search.js +3 -5
- package/dist/lib/search.js.map +1 -1
- package/dist/lib/useApiData.d.ts.map +1 -1
- package/dist/lib/useApiData.js.map +1 -1
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js.map +1 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +1 -3
- package/dist/router/index.js.map +1 -1
- package/dist/views/CollectionList.d.ts.map +1 -1
- package/dist/views/CollectionList.js +56 -17
- package/dist/views/CollectionList.js.map +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +26 -13
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/DocumentEdit.d.ts +1 -1
- package/dist/views/DocumentEdit.d.ts.map +1 -1
- package/dist/views/DocumentEdit.js +33 -15
- package/dist/views/DocumentEdit.js.map +1 -1
- package/dist/views/ForgotPassword.d.ts.map +1 -1
- package/dist/views/ForgotPassword.js.map +1 -1
- package/dist/views/FormEditor.d.ts.map +1 -1
- package/dist/views/FormEditor.js +8 -2
- package/dist/views/FormEditor.js.map +1 -1
- package/dist/views/FormSubmissions.d.ts.map +1 -1
- package/dist/views/FormSubmissions.js +6 -6
- package/dist/views/FormSubmissions.js.map +1 -1
- package/dist/views/Forms.d.ts.map +1 -1
- package/dist/views/Forms.js.map +1 -1
- package/dist/views/Login.d.ts.map +1 -1
- package/dist/views/Login.js +5 -2
- package/dist/views/Login.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +39 -19
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/PageEditor.d.ts.map +1 -1
- package/dist/views/PageEditor.js.map +1 -1
- package/dist/views/Pages.d.ts.map +1 -1
- package/dist/views/Pages.js +20 -10
- package/dist/views/Pages.js.map +1 -1
- package/dist/views/PostEditor.d.ts.map +1 -1
- package/dist/views/PostEditor.js.map +1 -1
- package/dist/views/Posts.d.ts.map +1 -1
- package/dist/views/Posts.js +13 -7
- package/dist/views/Posts.js.map +1 -1
- package/dist/views/Redirects.d.ts.map +1 -1
- package/dist/views/Redirects.js +17 -5
- package/dist/views/Redirects.js.map +1 -1
- package/dist/views/ResetPassword.d.ts.map +1 -1
- package/dist/views/ResetPassword.js.map +1 -1
- package/dist/views/SEO.d.ts.map +1 -1
- package/dist/views/SEO.js +39 -16
- package/dist/views/SEO.js.map +1 -1
- package/dist/views/ScriptTagEditor.d.ts.map +1 -1
- package/dist/views/ScriptTagEditor.js +2 -1
- package/dist/views/ScriptTagEditor.js.map +1 -1
- package/dist/views/ScriptTags.d.ts.map +1 -1
- package/dist/views/ScriptTags.js.map +1 -1
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +38 -11
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/SetupWizard.d.ts.map +1 -1
- package/dist/views/SetupWizard.js.map +1 -1
- package/dist/views/Users.d.ts.map +1 -1
- package/dist/views/Users.js +5 -3
- package/dist/views/Users.js.map +1 -1
- package/dist/views/page-builder/AIBlockAssist.d.ts.map +1 -1
- package/dist/views/page-builder/AIBlockAssist.js +1 -1
- package/dist/views/page-builder/AIBlockAssist.js.map +1 -1
- package/dist/views/page-builder/AIGenerateDialog.d.ts.map +1 -1
- package/dist/views/page-builder/AIGenerateDialog.js +4 -1
- package/dist/views/page-builder/AIGenerateDialog.js.map +1 -1
- package/dist/views/page-builder/BlockEditor.d.ts.map +1 -1
- package/dist/views/page-builder/BlockEditor.js +1 -1
- package/dist/views/page-builder/BlockEditor.js.map +1 -1
- package/dist/views/page-builder/BlockPicker.d.ts.map +1 -1
- package/dist/views/page-builder/BlockPicker.js.map +1 -1
- package/dist/views/page-builder/BottomBar.d.ts.map +1 -1
- package/dist/views/page-builder/BottomBar.js.map +1 -1
- package/dist/views/page-builder/BuilderToolbar.d.ts.map +1 -1
- package/dist/views/page-builder/BuilderToolbar.js.map +1 -1
- package/dist/views/page-builder/ContextPanel.d.ts.map +1 -1
- package/dist/views/page-builder/ContextPanel.js +4 -1
- package/dist/views/page-builder/ContextPanel.js.map +1 -1
- package/dist/views/page-builder/DesignScore.d.ts.map +1 -1
- package/dist/views/page-builder/DesignScore.js.map +1 -1
- package/dist/views/page-builder/NodeSettings.d.ts.map +1 -1
- package/dist/views/page-builder/NodeSettings.js +1 -1
- package/dist/views/page-builder/NodeSettings.js.map +1 -1
- package/dist/views/page-builder/PageBuilder.d.ts +1 -1
- package/dist/views/page-builder/PageBuilder.d.ts.map +1 -1
- package/dist/views/page-builder/PageBuilder.js +4 -2
- package/dist/views/page-builder/PageBuilder.js.map +1 -1
- package/dist/views/page-builder/PageSettings.d.ts.map +1 -1
- package/dist/views/page-builder/PageSettings.js.map +1 -1
- package/dist/views/page-builder/PageTemplates.d.ts.map +1 -1
- package/dist/views/page-builder/PageTemplates.js.map +1 -1
- package/dist/views/page-builder/SEOPanel.d.ts.map +1 -1
- package/dist/views/page-builder/SEOPanel.js +1 -3
- package/dist/views/page-builder/SEOPanel.js.map +1 -1
- package/dist/views/page-builder/SavedSections.d.ts.map +1 -1
- package/dist/views/page-builder/SavedSections.js +3 -7
- package/dist/views/page-builder/SavedSections.js.map +1 -1
- package/dist/views/page-builder/TemplatePicker.d.ts.map +1 -1
- package/dist/views/page-builder/TemplatePicker.js.map +1 -1
- package/dist/views/page-builder/block-renderers/CTAPreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/CTAPreview.js +1 -1
- package/dist/views/page-builder/block-renderers/CTAPreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/CardsPreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/CardsPreview.js +1 -1
- package/dist/views/page-builder/block-renderers/CardsPreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/CodePreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/CodePreview.js +1 -5
- package/dist/views/page-builder/block-renderers/CodePreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/FAQPreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/FAQPreview.js +4 -1
- package/dist/views/page-builder/block-renderers/FAQPreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/FallbackPreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/FormPreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/FormPreview.js +2 -2
- package/dist/views/page-builder/block-renderers/FormPreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/GalleryPreview.js +1 -3
- package/dist/views/page-builder/block-renderers/GalleryPreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/HeroPreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/HeroPreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/ImagePreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/ImagePreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/TextPreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/TextPreview.js +2 -6
- package/dist/views/page-builder/block-renderers/TextPreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/VideoPreview.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/VideoPreview.js +2 -5
- package/dist/views/page-builder/block-renderers/VideoPreview.js.map +1 -1
- package/dist/views/page-builder/block-renderers/index.d.ts.map +1 -1
- package/dist/views/page-builder/block-renderers/index.js.map +1 -1
- package/dist/views/page-builder/canvas/BlockRenderer.d.ts.map +1 -1
- package/dist/views/page-builder/canvas/BlockRenderer.js +1 -5
- package/dist/views/page-builder/canvas/BlockRenderer.js.map +1 -1
- package/dist/views/page-builder/canvas/BuilderCanvas.d.ts.map +1 -1
- package/dist/views/page-builder/canvas/BuilderCanvas.js.map +1 -1
- package/dist/views/page-builder/canvas/ColumnRenderer.d.ts.map +1 -1
- package/dist/views/page-builder/canvas/ColumnRenderer.js +1 -5
- package/dist/views/page-builder/canvas/ColumnRenderer.js.map +1 -1
- package/dist/views/page-builder/canvas/ContainerRenderer.d.ts.map +1 -1
- package/dist/views/page-builder/canvas/ContainerRenderer.js +1 -5
- package/dist/views/page-builder/canvas/ContainerRenderer.js.map +1 -1
- package/dist/views/page-builder/canvas/RowRenderer.d.ts.map +1 -1
- package/dist/views/page-builder/canvas/RowRenderer.js +1 -5
- package/dist/views/page-builder/canvas/RowRenderer.js.map +1 -1
- package/dist/views/page-builder/canvas/SectionRenderer.d.ts.map +1 -1
- package/dist/views/page-builder/canvas/SectionRenderer.js +1 -5
- package/dist/views/page-builder/canvas/SectionRenderer.js.map +1 -1
- package/dist/views/page-builder/canvas/index.d.ts.map +1 -1
- package/dist/views/page-builder/canvas/index.js.map +1 -1
- package/package.json +2 -2
- package/src/AdminRoot.tsx +263 -191
- package/src/__tests__/lib/search.test.ts +60 -69
- package/src/__tests__/lib/utils.test.ts +12 -12
- package/src/__tests__/router/match-route.test.ts +24 -26
- package/src/__tests__/router/strip-base.test.ts +15 -15
- package/src/components/Breadcrumbs.tsx +27 -24
- package/src/components/CommandPalette.tsx +115 -99
- package/src/components/ContentOverviewChart.tsx +19 -14
- package/src/components/ErrorBoundary.tsx +13 -13
- package/src/components/FocalPointPicker.tsx +31 -20
- package/src/components/FolderTree.tsx +172 -139
- package/src/components/LivePreview.tsx +68 -41
- package/src/components/LocaleProvider.tsx +26 -20
- package/src/components/LocaleSwitcher.tsx +9 -11
- package/src/components/MediaPickerModal.tsx +46 -45
- package/src/components/PresenceIndicator.tsx +30 -27
- package/src/components/SEOPanel.tsx +378 -228
- package/src/components/SEOPerformance.tsx +52 -30
- package/src/components/ThemeProvider.tsx +46 -46
- package/src/components/TipTapEditor.tsx +60 -64
- package/src/components/VersionHistory.tsx +63 -52
- package/src/components/ui/Avatar.tsx +8 -8
- package/src/components/ui/Badge.tsx +7 -5
- package/src/components/ui/Button.tsx +24 -13
- package/src/components/ui/CommandPalette.tsx +56 -42
- package/src/components/ui/ConfirmDialog.tsx +14 -14
- package/src/components/ui/DataTable.tsx +37 -39
- package/src/components/ui/EmptyState.tsx +9 -11
- package/src/components/ui/Modal.tsx +21 -15
- package/src/components/ui/Pagination.tsx +34 -19
- package/src/components/ui/SearchInput.tsx +17 -7
- package/src/components/ui/Skeleton.tsx +7 -7
- package/src/components/ui/Toast.tsx +29 -22
- package/src/components/ui/index.ts +24 -24
- package/src/fields/ArrayField.tsx +43 -25
- package/src/fields/BlockBuilderField.tsx +80 -99
- package/src/fields/DateField.tsx +20 -12
- package/src/fields/FieldRenderer.tsx +34 -34
- package/src/fields/GroupField.tsx +8 -10
- package/src/fields/MediaField.tsx +8 -10
- package/src/fields/NavBuilderField.tsx +24 -25
- package/src/fields/NumberField.tsx +21 -14
- package/src/fields/RelationshipField.tsx +105 -91
- package/src/fields/RichTextField.tsx +16 -12
- package/src/fields/SelectField.tsx +42 -34
- package/src/fields/SlugField.tsx +29 -17
- package/src/fields/TextField.tsx +24 -16
- package/src/fields/ToggleField.tsx +7 -9
- package/src/fields/block-types.ts +50 -24
- package/src/fields/index.ts +17 -17
- package/src/hooks/useBuilderState.ts +260 -221
- package/src/hooks/useContentLock.ts +23 -20
- package/src/hooks/useDebounce.ts +7 -7
- package/src/hooks/useKeyboardShortcuts.ts +16 -16
- package/src/index.ts +69 -58
- package/src/layout/Header.tsx +21 -20
- package/src/layout/Layout.tsx +22 -24
- package/src/layout/Sidebar.tsx +107 -72
- package/src/lib/api.ts +34 -34
- package/src/lib/search.ts +30 -34
- package/src/lib/useApiData.ts +65 -62
- package/src/lib/utils.ts +3 -3
- package/src/router/index.ts +33 -35
- package/src/styles/build-input.css +2 -2
- package/src/styles/tailwind.css +1 -1
- package/src/styles/theme.css +7 -1
- package/src/views/CollectionList.tsx +275 -121
- package/src/views/Dashboard.tsx +164 -117
- package/src/views/DocumentEdit.tsx +298 -253
- package/src/views/ForgotPassword.tsx +27 -23
- package/src/views/FormEditor.tsx +165 -99
- package/src/views/FormSubmissions.tsx +261 -117
- package/src/views/Forms.tsx +56 -26
- package/src/views/Login.tsx +107 -84
- package/src/views/MediaBrowser.tsx +717 -523
- package/src/views/PageEditor.tsx +44 -46
- package/src/views/Pages.tsx +312 -149
- package/src/views/PostEditor.tsx +57 -51
- package/src/views/Posts.tsx +206 -74
- package/src/views/Redirects.tsx +173 -117
- package/src/views/ResetPassword.tsx +43 -32
- package/src/views/SEO.tsx +589 -160
- package/src/views/ScriptTagEditor.tsx +69 -69
- package/src/views/ScriptTags.tsx +54 -42
- package/src/views/Settings.tsx +430 -220
- package/src/views/SetupWizard.tsx +69 -46
- package/src/views/Users.tsx +154 -120
- package/src/views/page-builder/AIBlockAssist.tsx +21 -25
- package/src/views/page-builder/AIGenerateDialog.tsx +134 -127
- package/src/views/page-builder/BlockEditor.tsx +94 -96
- package/src/views/page-builder/BlockPicker.tsx +73 -88
- package/src/views/page-builder/BottomBar.tsx +15 -11
- package/src/views/page-builder/BuilderToolbar.tsx +32 -29
- package/src/views/page-builder/ContextPanel.tsx +57 -57
- package/src/views/page-builder/DesignScore.tsx +52 -59
- package/src/views/page-builder/NodeSettings.tsx +59 -59
- package/src/views/page-builder/PageBuilder.tsx +156 -155
- package/src/views/page-builder/PageSettings.tsx +16 -15
- package/src/views/page-builder/PageTemplates.tsx +23 -17
- package/src/views/page-builder/SEOPanel.tsx +90 -111
- package/src/views/page-builder/SavedSections.tsx +99 -105
- package/src/views/page-builder/TemplatePicker.tsx +44 -48
- package/src/views/page-builder/block-renderers/CTAPreview.tsx +11 -13
- package/src/views/page-builder/block-renderers/CardsPreview.tsx +13 -15
- package/src/views/page-builder/block-renderers/CodePreview.tsx +16 -16
- package/src/views/page-builder/block-renderers/FAQPreview.tsx +20 -23
- package/src/views/page-builder/block-renderers/FallbackPreview.tsx +5 -5
- package/src/views/page-builder/block-renderers/FormPreview.tsx +9 -13
- package/src/views/page-builder/block-renderers/GalleryPreview.tsx +22 -28
- package/src/views/page-builder/block-renderers/HeroPreview.tsx +17 -30
- package/src/views/page-builder/block-renderers/ImagePreview.tsx +12 -12
- package/src/views/page-builder/block-renderers/TextPreview.tsx +22 -22
- package/src/views/page-builder/block-renderers/VideoPreview.tsx +13 -18
- package/src/views/page-builder/block-renderers/index.ts +17 -17
- package/src/views/page-builder/canvas/BlockRenderer.tsx +19 -23
- package/src/views/page-builder/canvas/BuilderCanvas.tsx +17 -20
- package/src/views/page-builder/canvas/ColumnRenderer.tsx +22 -26
- package/src/views/page-builder/canvas/ContainerRenderer.tsx +20 -24
- package/src/views/page-builder/canvas/RowRenderer.tsx +19 -23
- package/src/views/page-builder/canvas/SectionRenderer.tsx +30 -34
- package/src/views/page-builder/canvas/index.ts +2 -2
|
@@ -1,44 +1,66 @@
|
|
|
1
|
-
'use client'
|
|
1
|
+
'use client'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
Upload,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
4
|
+
Upload,
|
|
5
|
+
Grid3x3,
|
|
6
|
+
List,
|
|
7
|
+
Search,
|
|
8
|
+
Trash2,
|
|
9
|
+
Download,
|
|
10
|
+
FileText,
|
|
11
|
+
ArrowUpDown,
|
|
12
|
+
ArrowUp,
|
|
13
|
+
ArrowDown,
|
|
14
|
+
X,
|
|
15
|
+
Bot,
|
|
16
|
+
Sparkles,
|
|
17
|
+
Link2,
|
|
18
|
+
AlertTriangle,
|
|
19
|
+
Copy,
|
|
20
|
+
ExternalLink,
|
|
21
|
+
ImageIcon,
|
|
22
|
+
FileImage,
|
|
23
|
+
Loader2,
|
|
24
|
+
FolderInput,
|
|
25
|
+
GripVertical,
|
|
26
|
+
} from 'lucide-react'
|
|
27
|
+
import { useState, useMemo, useRef, useCallback } from 'react'
|
|
28
|
+
import { toast } from 'sonner'
|
|
29
|
+
import { sortByRelevance, type SortConfig, toggleSort } from '../lib/search.js'
|
|
30
|
+
import { useApiData } from '../lib/useApiData.js'
|
|
31
|
+
import { cmsApi } from '../lib/api.js'
|
|
32
|
+
import { FolderTree, type FolderSelection } from '../components/FolderTree.js'
|
|
33
|
+
import { FocalPointPicker } from '../components/FocalPointPicker.js'
|
|
16
34
|
|
|
17
35
|
interface MediaItem {
|
|
18
|
-
id: number | string
|
|
19
|
-
name: string
|
|
20
|
-
type: string
|
|
21
|
-
size: string
|
|
22
|
-
sizeBytes: number
|
|
23
|
-
date: string
|
|
24
|
-
url: string
|
|
25
|
-
dimensions?: string
|
|
26
|
-
format?: string
|
|
27
|
-
altTag?: string
|
|
28
|
-
title?: string
|
|
29
|
-
usedOn?: { page: string; path: string }[]
|
|
36
|
+
id: number | string
|
|
37
|
+
name: string
|
|
38
|
+
type: string
|
|
39
|
+
size: string
|
|
40
|
+
sizeBytes: number
|
|
41
|
+
date: string
|
|
42
|
+
url: string
|
|
43
|
+
dimensions?: string
|
|
44
|
+
format?: string
|
|
45
|
+
altTag?: string
|
|
46
|
+
title?: string
|
|
47
|
+
usedOn?: { page: string; path: string }[]
|
|
30
48
|
}
|
|
31
49
|
|
|
32
50
|
function isImageMedia(item: MediaItem): boolean {
|
|
33
|
-
return
|
|
51
|
+
return (
|
|
52
|
+
Boolean(item.url) &&
|
|
53
|
+
(item.type?.startsWith('image/') || /\.(avif|gif|jpe?g|png|webp|svg)$/i.test(item.url))
|
|
54
|
+
)
|
|
34
55
|
}
|
|
35
56
|
|
|
36
57
|
function matchesMediaType(item: MediaItem, filterType: string): boolean {
|
|
37
|
-
if (filterType === 'all') return true
|
|
38
|
-
if (filterType === 'image') return item.type?.startsWith('image/') || isImageMedia(item)
|
|
39
|
-
if (filterType === 'video') return item.type?.startsWith('video/')
|
|
40
|
-
if (filterType === 'document')
|
|
41
|
-
|
|
58
|
+
if (filterType === 'all') return true
|
|
59
|
+
if (filterType === 'image') return item.type?.startsWith('image/') || isImageMedia(item)
|
|
60
|
+
if (filterType === 'video') return item.type?.startsWith('video/')
|
|
61
|
+
if (filterType === 'document')
|
|
62
|
+
return !item.type?.startsWith('image/') && !item.type?.startsWith('video/')
|
|
63
|
+
return item.type === filterType
|
|
42
64
|
}
|
|
43
65
|
|
|
44
66
|
function MediaPreview({ item }: { item: MediaItem }) {
|
|
@@ -50,105 +72,115 @@ function MediaPreview({ item }: { item: MediaItem }) {
|
|
|
50
72
|
className="h-full w-full object-cover"
|
|
51
73
|
loading="lazy"
|
|
52
74
|
/>
|
|
53
|
-
)
|
|
75
|
+
)
|
|
54
76
|
}
|
|
55
77
|
|
|
56
|
-
return <FileImage className="w-6 h-6 sm:w-8 sm:h-8 text-muted-foreground"
|
|
78
|
+
return <FileImage className="w-6 h-6 sm:w-8 sm:h-8 text-muted-foreground" />
|
|
57
79
|
}
|
|
58
80
|
|
|
59
|
-
type MediaSortKey = 'name' | 'type' | 'size' | 'date'
|
|
81
|
+
type MediaSortKey = 'name' | 'type' | 'size' | 'date'
|
|
60
82
|
|
|
61
83
|
export interface MediaBrowserProps {
|
|
62
|
-
onNavigate?: (path: string) => void
|
|
84
|
+
onNavigate?: (path: string) => void
|
|
63
85
|
}
|
|
64
86
|
|
|
65
87
|
function buildMediaApiUrl(folderSel: FolderSelection): string {
|
|
66
|
-
const base = '/media?pageSize=100'
|
|
88
|
+
const base = '/media?pageSize=100'
|
|
67
89
|
if (folderSel.type === 'smart') {
|
|
68
|
-
if (folderSel.smart === 'recent') return `${base}&sort=updatedAt&order=desc&pageSize=20
|
|
69
|
-
if (folderSel.smart === 'uncategorized') return `${base}&folderId=none
|
|
70
|
-
return base
|
|
90
|
+
if (folderSel.smart === 'recent') return `${base}&sort=updatedAt&order=desc&pageSize=20`
|
|
91
|
+
if (folderSel.smart === 'uncategorized') return `${base}&folderId=none`
|
|
92
|
+
return base
|
|
71
93
|
}
|
|
72
|
-
return `${base}&folderId=${folderSel.folderId}
|
|
94
|
+
return `${base}&folderId=${folderSel.folderId}`
|
|
73
95
|
}
|
|
74
96
|
|
|
75
97
|
export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
76
|
-
const [folderSel, setFolderSel] = useState<FolderSelection>({ type: 'smart', smart: 'all' })
|
|
77
|
-
const [sidebarOpen, setSidebarOpen] = useState(true)
|
|
78
|
-
|
|
79
|
-
const apiUrl = useMemo(() => buildMediaApiUrl(folderSel), [folderSel])
|
|
80
|
-
const { data, loading, error, refetch } = useApiData<{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const [
|
|
87
|
-
const [
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const [
|
|
93
|
-
const [
|
|
94
|
-
const [
|
|
95
|
-
const [
|
|
96
|
-
const [
|
|
97
|
-
|
|
98
|
-
const [
|
|
99
|
-
const [
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
const
|
|
98
|
+
const [folderSel, setFolderSel] = useState<FolderSelection>({ type: 'smart', smart: 'all' })
|
|
99
|
+
const [sidebarOpen, setSidebarOpen] = useState(true)
|
|
100
|
+
|
|
101
|
+
const apiUrl = useMemo(() => buildMediaApiUrl(folderSel), [folderSel])
|
|
102
|
+
const { data, loading, error, refetch } = useApiData<{
|
|
103
|
+
data?: MediaItem[]
|
|
104
|
+
items?: MediaItem[]
|
|
105
|
+
total: number
|
|
106
|
+
}>(apiUrl)
|
|
107
|
+
|
|
108
|
+
const allData = useApiData<{ data: MediaItem[]; total: number }>('/media?pageSize=1')
|
|
109
|
+
const uncatData = useApiData<{ data: MediaItem[]; total: number }>(
|
|
110
|
+
'/media?pageSize=1&folderId=none',
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
|
|
114
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
115
|
+
const [filterType, setFilterType] = useState('all')
|
|
116
|
+
const [selectedMedia, setSelectedMedia] = useState<Array<number | string>>([])
|
|
117
|
+
const [sortConfig, setSortConfig] = useState<SortConfig<MediaSortKey> | null>(null)
|
|
118
|
+
const [activeItem, setActiveItem] = useState<MediaItem | null>(null)
|
|
119
|
+
|
|
120
|
+
const [editAlt, setEditAlt] = useState('')
|
|
121
|
+
const [editTitle, setEditTitle] = useState('')
|
|
122
|
+
const [editFilename, setEditFilename] = useState('')
|
|
123
|
+
const [focalX, setFocalX] = useState(0.5)
|
|
124
|
+
const [focalY, setFocalY] = useState(0.5)
|
|
125
|
+
const [saving, setSaving] = useState(false)
|
|
126
|
+
const [aiGenerating, setAiGenerating] = useState<string | null>(null)
|
|
127
|
+
const [uploading, setUploading] = useState(false)
|
|
128
|
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
129
|
+
|
|
130
|
+
const mediaItems = data?.data ?? data?.items ?? []
|
|
103
131
|
|
|
104
132
|
const filteredAndSorted = useMemo(() => {
|
|
105
133
|
let results = mediaItems.filter((item) => {
|
|
106
|
-
const matchesSearch = (item.name ?? '').toLowerCase().includes(searchQuery.toLowerCase())
|
|
107
|
-
const matchesType = matchesMediaType(item, filterType)
|
|
108
|
-
return matchesSearch && matchesType
|
|
109
|
-
})
|
|
134
|
+
const matchesSearch = (item.name ?? '').toLowerCase().includes(searchQuery.toLowerCase())
|
|
135
|
+
const matchesType = matchesMediaType(item, filterType)
|
|
136
|
+
return matchesSearch && matchesType
|
|
137
|
+
})
|
|
110
138
|
|
|
111
139
|
if (searchQuery.trim()) {
|
|
112
|
-
results = sortByRelevance(results, searchQuery, (m) => [m.name])
|
|
140
|
+
results = sortByRelevance(results, searchQuery, (m) => [m.name])
|
|
113
141
|
} else if (sortConfig) {
|
|
114
142
|
results = [...results].sort((a, b) => {
|
|
115
|
-
let cmp: number
|
|
143
|
+
let cmp: number
|
|
116
144
|
if (sortConfig.key === 'size') {
|
|
117
|
-
cmp = a.sizeBytes - b.sizeBytes
|
|
145
|
+
cmp = a.sizeBytes - b.sizeBytes
|
|
118
146
|
} else {
|
|
119
|
-
cmp = String(a[sortConfig.key]).localeCompare(String(b[sortConfig.key]))
|
|
147
|
+
cmp = String(a[sortConfig.key]).localeCompare(String(b[sortConfig.key]))
|
|
120
148
|
}
|
|
121
|
-
return sortConfig.direction === 'asc' ? cmp : -cmp
|
|
122
|
-
})
|
|
149
|
+
return sortConfig.direction === 'asc' ? cmp : -cmp
|
|
150
|
+
})
|
|
123
151
|
}
|
|
124
|
-
return results
|
|
125
|
-
}, [mediaItems, searchQuery, filterType, sortConfig])
|
|
152
|
+
return results
|
|
153
|
+
}, [mediaItems, searchQuery, filterType, sortConfig])
|
|
126
154
|
|
|
127
155
|
const openDetail = (item: MediaItem) => {
|
|
128
|
-
setActiveItem(item)
|
|
129
|
-
setEditAlt(item.altTag ?? '')
|
|
130
|
-
setEditTitle(item.title ?? '')
|
|
131
|
-
setEditFilename(item.name)
|
|
132
|
-
setFocalX((item as any).focalPointX ?? 0.5)
|
|
133
|
-
setFocalY((item as any).focalPointY ?? 0.5)
|
|
134
|
-
}
|
|
156
|
+
setActiveItem(item)
|
|
157
|
+
setEditAlt(item.altTag ?? '')
|
|
158
|
+
setEditTitle(item.title ?? '')
|
|
159
|
+
setEditFilename(item.name)
|
|
160
|
+
setFocalX((item as any).focalPointX ?? 0.5)
|
|
161
|
+
setFocalY((item as any).focalPointY ?? 0.5)
|
|
162
|
+
}
|
|
135
163
|
|
|
136
164
|
const closeDetail = () => {
|
|
137
|
-
setActiveItem(null)
|
|
138
|
-
}
|
|
165
|
+
setActiveItem(null)
|
|
166
|
+
}
|
|
139
167
|
|
|
140
168
|
const handleCheckbox = (e: React.MouseEvent, id: number | string) => {
|
|
141
|
-
e.stopPropagation()
|
|
142
|
-
setSelectedMedia(prev
|
|
143
|
-
|
|
169
|
+
e.stopPropagation()
|
|
170
|
+
setSelectedMedia((prev) =>
|
|
171
|
+
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
|
|
172
|
+
)
|
|
173
|
+
}
|
|
144
174
|
|
|
145
175
|
const handleSelectAll = () => {
|
|
146
|
-
setSelectedMedia(prev =>
|
|
147
|
-
|
|
176
|
+
setSelectedMedia((prev) =>
|
|
177
|
+
prev.length === filteredAndSorted.length ? [] : filteredAndSorted.map((item) => item.id),
|
|
178
|
+
)
|
|
179
|
+
}
|
|
148
180
|
|
|
149
181
|
const handleSaveDetails = async () => {
|
|
150
|
-
if (!activeItem) return
|
|
151
|
-
setSaving(true)
|
|
182
|
+
if (!activeItem) return
|
|
183
|
+
setSaving(true)
|
|
152
184
|
const res = await cmsApi(`/media/${activeItem.id}`, {
|
|
153
185
|
method: 'PUT',
|
|
154
186
|
body: JSON.stringify({
|
|
@@ -158,147 +190,160 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
158
190
|
focalX,
|
|
159
191
|
focalY,
|
|
160
192
|
}),
|
|
161
|
-
})
|
|
162
|
-
setSaving(false)
|
|
193
|
+
})
|
|
194
|
+
setSaving(false)
|
|
163
195
|
if (res.error) {
|
|
164
|
-
toast.error(res.error)
|
|
196
|
+
toast.error(res.error)
|
|
165
197
|
} else {
|
|
166
|
-
toast.success('Media details saved')
|
|
167
|
-
refetch()
|
|
198
|
+
toast.success('Media details saved')
|
|
199
|
+
refetch()
|
|
168
200
|
}
|
|
169
|
-
}
|
|
201
|
+
}
|
|
170
202
|
|
|
171
203
|
const deleteMedia = async (id: number | string) => {
|
|
172
|
-
const res = await cmsApi(`/media/${id}`, { method: 'DELETE' })
|
|
204
|
+
const res = await cmsApi(`/media/${id}`, { method: 'DELETE' })
|
|
173
205
|
if (res.error) {
|
|
174
|
-
toast.error(res.error)
|
|
206
|
+
toast.error(res.error)
|
|
175
207
|
} else {
|
|
176
|
-
toast.success('Media deleted')
|
|
177
|
-
if (activeItem?.id === id) closeDetail()
|
|
178
|
-
refetch()
|
|
208
|
+
toast.success('Media deleted')
|
|
209
|
+
if (activeItem?.id === id) closeDetail()
|
|
210
|
+
refetch()
|
|
179
211
|
}
|
|
180
|
-
}
|
|
212
|
+
}
|
|
181
213
|
|
|
182
214
|
const handleAiGenerate = async (field: 'alt' | 'title' | 'optimize') => {
|
|
183
|
-
setAiGenerating(field)
|
|
215
|
+
setAiGenerating(field)
|
|
184
216
|
|
|
185
217
|
if (field === 'optimize' && activeItem) {
|
|
186
|
-
const res = await cmsApi<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
218
|
+
const res = await cmsApi<
|
|
219
|
+
MediaItem & {
|
|
220
|
+
optimization?: {
|
|
221
|
+
originalSize: number
|
|
222
|
+
optimizedSize: number
|
|
223
|
+
savings: number
|
|
224
|
+
originalSizeFormatted: string
|
|
225
|
+
optimizedSizeFormatted: string
|
|
226
|
+
alreadyOptimized?: boolean
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
>(`/media/${activeItem.id}/optimize`, { method: 'POST' })
|
|
196
230
|
|
|
197
231
|
if (res.error) {
|
|
198
|
-
toast.error(res.error)
|
|
232
|
+
toast.error(res.error)
|
|
199
233
|
} else if ((res.data as any)?.optimization?.alreadyOptimized) {
|
|
200
|
-
toast.info('Image is already in WebP format — no further optimization needed')
|
|
234
|
+
toast.info('Image is already in WebP format — no further optimization needed')
|
|
201
235
|
} else if ((res.data as any)?.optimization) {
|
|
202
|
-
const opt = (res.data as any).optimization
|
|
236
|
+
const opt = (res.data as any).optimization
|
|
203
237
|
toast.success(
|
|
204
238
|
`Optimized: ${opt.originalSizeFormatted} → ${opt.optimizedSizeFormatted} (${opt.savings}% smaller)`,
|
|
205
|
-
)
|
|
206
|
-
refetch()
|
|
239
|
+
)
|
|
240
|
+
refetch()
|
|
207
241
|
}
|
|
208
242
|
|
|
209
|
-
setAiGenerating(null)
|
|
210
|
-
return
|
|
243
|
+
setAiGenerating(null)
|
|
244
|
+
return
|
|
211
245
|
}
|
|
212
246
|
|
|
213
|
-
await new Promise(r => setTimeout(r, 1500))
|
|
247
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
214
248
|
if (field === 'alt') {
|
|
215
|
-
const generated = `A ${(activeItem?.format ?? 'media').toLowerCase()} image showing ${(activeItem?.name ?? 'uploaded').replace(/[-_]/g, ' ').replace(/\.\w+$/, '')} content
|
|
216
|
-
setEditAlt(generated)
|
|
217
|
-
toast.success('Alt tag generated by AI')
|
|
249
|
+
const generated = `A ${(activeItem?.format ?? 'media').toLowerCase()} image showing ${(activeItem?.name ?? 'uploaded').replace(/[-_]/g, ' ').replace(/\.\w+$/, '')} content`
|
|
250
|
+
setEditAlt(generated)
|
|
251
|
+
toast.success('Alt tag generated by AI')
|
|
218
252
|
} else if (field === 'title') {
|
|
219
|
-
const generated =
|
|
220
|
-
|
|
221
|
-
|
|
253
|
+
const generated =
|
|
254
|
+
activeItem?.name
|
|
255
|
+
.replace(/[-_]/g, ' ')
|
|
256
|
+
.replace(/\.\w+$/, '')
|
|
257
|
+
.replace(/\b\w/g, (c) => c.toUpperCase()) ?? ''
|
|
258
|
+
setEditTitle(generated)
|
|
259
|
+
toast.success('Title generated by AI')
|
|
222
260
|
}
|
|
223
|
-
setAiGenerating(null)
|
|
224
|
-
}
|
|
261
|
+
setAiGenerating(null)
|
|
262
|
+
}
|
|
225
263
|
|
|
226
264
|
const handleCopyUrl = () => {
|
|
227
265
|
if (activeItem?.url) {
|
|
228
|
-
navigator.clipboard.writeText(activeItem.url)
|
|
229
|
-
toast.success('URL copied to clipboard')
|
|
266
|
+
navigator.clipboard.writeText(activeItem.url)
|
|
267
|
+
toast.success('URL copied to clipboard')
|
|
230
268
|
}
|
|
231
|
-
}
|
|
269
|
+
}
|
|
232
270
|
|
|
233
271
|
const handleUploadFiles = async (files: FileList | null) => {
|
|
234
|
-
if (!files || files.length === 0) return
|
|
235
|
-
setUploading(true)
|
|
272
|
+
if (!files || files.length === 0) return
|
|
273
|
+
setUploading(true)
|
|
236
274
|
|
|
237
|
-
let successCount = 0
|
|
275
|
+
let successCount = 0
|
|
238
276
|
|
|
239
277
|
for (let i = 0; i < files.length; i++) {
|
|
240
|
-
const file = files[i]
|
|
241
|
-
const formData = new FormData()
|
|
242
|
-
formData.append('file', file)
|
|
243
|
-
|
|
244
|
-
const res = await cmsApi<
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
278
|
+
const file = files[i]!
|
|
279
|
+
const formData = new FormData()
|
|
280
|
+
formData.append('file', file)
|
|
281
|
+
|
|
282
|
+
const res = await cmsApi<
|
|
283
|
+
MediaItem & {
|
|
284
|
+
optimization?: {
|
|
285
|
+
originalSize: number
|
|
286
|
+
optimizedSize: number
|
|
287
|
+
savings: number
|
|
288
|
+
originalSizeFormatted: string
|
|
289
|
+
optimizedSizeFormatted: string
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
>('/media/upload', { method: 'POST', body: formData })
|
|
253
293
|
|
|
254
294
|
if (res.error) {
|
|
255
|
-
toast.error(`Failed to upload ${file.name}: ${res.error}`)
|
|
295
|
+
toast.error(`Failed to upload ${file.name}: ${res.error}`)
|
|
256
296
|
} else {
|
|
257
|
-
const opt = (res.data as any)?.optimization
|
|
297
|
+
const opt = (res.data as any)?.optimization
|
|
258
298
|
if (opt && opt.savings > 0) {
|
|
259
299
|
toast.success(
|
|
260
300
|
`${file.name} → WebP (${opt.originalSizeFormatted} → ${opt.optimizedSizeFormatted}, ${opt.savings}% saved)`,
|
|
261
|
-
)
|
|
301
|
+
)
|
|
262
302
|
} else {
|
|
263
|
-
toast.success(`Uploaded ${file.name}`)
|
|
303
|
+
toast.success(`Uploaded ${file.name}`)
|
|
264
304
|
}
|
|
265
|
-
successCount
|
|
305
|
+
successCount++
|
|
266
306
|
}
|
|
267
307
|
}
|
|
268
308
|
|
|
269
|
-
if (successCount > 0) refetch()
|
|
270
|
-
if (fileInputRef.current) fileInputRef.current.value = ''
|
|
271
|
-
setUploading(false)
|
|
272
|
-
}
|
|
309
|
+
if (successCount > 0) refetch()
|
|
310
|
+
if (fileInputRef.current) fileInputRef.current.value = ''
|
|
311
|
+
setUploading(false)
|
|
312
|
+
}
|
|
273
313
|
|
|
274
|
-
const handleDropItem = useCallback(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
314
|
+
const handleDropItem = useCallback(
|
|
315
|
+
async (itemId: string, folderId: string | null) => {
|
|
316
|
+
const res = await cmsApi(`/media/${itemId}/folder`, {
|
|
317
|
+
method: 'PUT',
|
|
318
|
+
body: JSON.stringify({ folderId }),
|
|
319
|
+
})
|
|
320
|
+
if (res.error) {
|
|
321
|
+
toast.error(res.error)
|
|
322
|
+
} else {
|
|
323
|
+
toast.success(folderId ? 'Moved to folder' : 'Removed from folder')
|
|
324
|
+
refetch()
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
[refetch],
|
|
328
|
+
)
|
|
286
329
|
|
|
287
330
|
const handleDragStart = (e: React.DragEvent, id: number | string) => {
|
|
288
|
-
e.dataTransfer.setData('text/actuate-item-id', String(id))
|
|
289
|
-
e.dataTransfer.effectAllowed = 'move'
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const panelOpen = activeItem !== null
|
|
293
|
-
const issues = activeItem
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
331
|
+
e.dataTransfer.setData('text/actuate-item-id', String(id))
|
|
332
|
+
e.dataTransfer.effectAllowed = 'move'
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const panelOpen = activeItem !== null
|
|
336
|
+
const issues = activeItem
|
|
337
|
+
? [
|
|
338
|
+
...(!activeItem.altTag ? ['Missing alt tag'] : []),
|
|
339
|
+
...(!activeItem.title ? ['Missing title'] : []),
|
|
340
|
+
...(activeItem.sizeBytes > 2000000 ? ['File size over 2 MB — consider optimizing'] : []),
|
|
341
|
+
...(activeItem.usedOn?.length === 0 ? ['Not used on any page'] : []),
|
|
342
|
+
]
|
|
343
|
+
: []
|
|
299
344
|
|
|
300
345
|
function SortHeader({ label, sortKey }: { label: string; sortKey: MediaSortKey }) {
|
|
301
|
-
const active = sortConfig?.key === sortKey
|
|
346
|
+
const active = sortConfig?.key === sortKey
|
|
302
347
|
return (
|
|
303
348
|
<button
|
|
304
349
|
type="button"
|
|
@@ -307,12 +352,16 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
307
352
|
>
|
|
308
353
|
{label}
|
|
309
354
|
{active ? (
|
|
310
|
-
sortConfig!.direction === 'asc' ?
|
|
355
|
+
sortConfig!.direction === 'asc' ? (
|
|
356
|
+
<ArrowUp className="w-3 h-3" />
|
|
357
|
+
) : (
|
|
358
|
+
<ArrowDown className="w-3 h-3" />
|
|
359
|
+
)
|
|
311
360
|
) : (
|
|
312
361
|
<ArrowUpDown className="w-3 h-3 text-gray-400" />
|
|
313
362
|
)}
|
|
314
363
|
</button>
|
|
315
|
-
)
|
|
364
|
+
)
|
|
316
365
|
}
|
|
317
366
|
|
|
318
367
|
if (loading) {
|
|
@@ -320,7 +369,7 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
320
369
|
<div className="p-3 pr-6 sm:p-4 sm:pr-8 flex items-center justify-center h-64">
|
|
321
370
|
<Loader2 className="w-6 h-6 animate-spin text-blue-600" />
|
|
322
371
|
</div>
|
|
323
|
-
)
|
|
372
|
+
)
|
|
324
373
|
}
|
|
325
374
|
|
|
326
375
|
return (
|
|
@@ -329,7 +378,12 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
329
378
|
<div className="mb-4 flex items-center gap-3 rounded-lg border border-red-200 bg-red-50 p-3">
|
|
330
379
|
<AlertTriangle className="w-5 h-5 text-red-600 shrink-0" />
|
|
331
380
|
<span className="text-sm text-red-800 flex-1">{error}</span>
|
|
332
|
-
<button
|
|
381
|
+
<button
|
|
382
|
+
onClick={refetch}
|
|
383
|
+
className="px-3 py-1 text-sm text-red-700 border border-red-300 rounded-lg hover:bg-red-100 transition-colors"
|
|
384
|
+
>
|
|
385
|
+
Retry
|
|
386
|
+
</button>
|
|
333
387
|
</div>
|
|
334
388
|
)}
|
|
335
389
|
|
|
@@ -337,7 +391,7 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
337
391
|
<div className="flex items-center gap-3">
|
|
338
392
|
<button
|
|
339
393
|
type="button"
|
|
340
|
-
onClick={() => setSidebarOpen(prev => !prev)}
|
|
394
|
+
onClick={() => setSidebarOpen((prev) => !prev)}
|
|
341
395
|
className="p-1.5 rounded-lg hover:bg-gray-100 transition-colors"
|
|
342
396
|
title={sidebarOpen ? 'Hide folders' : 'Show folders'}
|
|
343
397
|
>
|
|
@@ -362,7 +416,11 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
362
416
|
disabled={uploading}
|
|
363
417
|
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm disabled:opacity-50"
|
|
364
418
|
>
|
|
365
|
-
{uploading ?
|
|
419
|
+
{uploading ? (
|
|
420
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
421
|
+
) : (
|
|
422
|
+
<Upload className="w-4 h-4" />
|
|
423
|
+
)}
|
|
366
424
|
{uploading ? 'Uploading...' : 'Upload Files'}
|
|
367
425
|
</button>
|
|
368
426
|
</div>
|
|
@@ -374,7 +432,10 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
374
432
|
<FolderTree
|
|
375
433
|
scope="media"
|
|
376
434
|
selected={folderSel}
|
|
377
|
-
onSelect={(sel) => {
|
|
435
|
+
onSelect={(sel) => {
|
|
436
|
+
setFolderSel(sel)
|
|
437
|
+
setSelectedMedia([])
|
|
438
|
+
}}
|
|
378
439
|
totalCount={allData.data?.total}
|
|
379
440
|
uncategorizedCount={uncatData.data?.total}
|
|
380
441
|
onDropItem={handleDropItem}
|
|
@@ -388,9 +449,19 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
388
449
|
<div className="flex items-center gap-3 flex-1">
|
|
389
450
|
<div className="flex-1 max-w-md relative">
|
|
390
451
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
|
391
|
-
<input
|
|
452
|
+
<input
|
|
453
|
+
type="text"
|
|
454
|
+
placeholder="Search media..."
|
|
455
|
+
value={searchQuery}
|
|
456
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
457
|
+
className="w-full pl-9 pr-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
458
|
+
/>
|
|
392
459
|
</div>
|
|
393
|
-
<select
|
|
460
|
+
<select
|
|
461
|
+
value={filterType}
|
|
462
|
+
onChange={(e) => setFilterType(e.target.value)}
|
|
463
|
+
className="px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
464
|
+
>
|
|
394
465
|
<option value="all">All Types</option>
|
|
395
466
|
<option value="image">Images</option>
|
|
396
467
|
<option value="video">Videos</option>
|
|
@@ -398,10 +469,16 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
398
469
|
</select>
|
|
399
470
|
</div>
|
|
400
471
|
<div className="flex items-center gap-1 bg-gray-100 p-1 rounded-lg">
|
|
401
|
-
<button
|
|
472
|
+
<button
|
|
473
|
+
onClick={() => setViewMode('grid')}
|
|
474
|
+
className={`p-1.5 rounded transition-colors ${viewMode === 'grid' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-600 hover:text-gray-900'}`}
|
|
475
|
+
>
|
|
402
476
|
<Grid3x3 className="w-4 h-4" />
|
|
403
477
|
</button>
|
|
404
|
-
<button
|
|
478
|
+
<button
|
|
479
|
+
onClick={() => setViewMode('list')}
|
|
480
|
+
className={`p-1.5 rounded transition-colors ${viewMode === 'list' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-600 hover:text-gray-900'}`}
|
|
481
|
+
>
|
|
405
482
|
<List className="w-4 h-4" />
|
|
406
483
|
</button>
|
|
407
484
|
</div>
|
|
@@ -411,10 +488,25 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
411
488
|
{selectedMedia.length > 0 && (
|
|
412
489
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3 mb-4">
|
|
413
490
|
<div className="flex items-center justify-between">
|
|
414
|
-
<span className="text-sm text-blue-900">
|
|
491
|
+
<span className="text-sm text-blue-900">
|
|
492
|
+
{selectedMedia.length} file{selectedMedia.length !== 1 ? 's' : ''} selected
|
|
493
|
+
</span>
|
|
415
494
|
<div className="flex items-center gap-2">
|
|
416
|
-
<button
|
|
417
|
-
|
|
495
|
+
<button
|
|
496
|
+
onClick={async () => {
|
|
497
|
+
for (const id of selectedMedia) await deleteMedia(id)
|
|
498
|
+
setSelectedMedia([])
|
|
499
|
+
}}
|
|
500
|
+
className="px-3 py-1.5 text-sm bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
|
|
501
|
+
>
|
|
502
|
+
Delete Selected
|
|
503
|
+
</button>
|
|
504
|
+
<button
|
|
505
|
+
onClick={() => setSelectedMedia([])}
|
|
506
|
+
className="px-3 py-1.5 text-sm border border-gray-300 bg-white rounded-lg hover:bg-gray-50 transition-colors"
|
|
507
|
+
>
|
|
508
|
+
Cancel
|
|
509
|
+
</button>
|
|
418
510
|
</div>
|
|
419
511
|
</div>
|
|
420
512
|
</div>
|
|
@@ -429,377 +521,479 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
429
521
|
{folderSel.type === 'smart' && folderSel.smart === 'uncategorized'
|
|
430
522
|
? 'No uncategorized media'
|
|
431
523
|
: folderSel.type === 'folder'
|
|
432
|
-
|
|
433
|
-
|
|
524
|
+
? 'No media in this folder'
|
|
525
|
+
: 'No media yet'}
|
|
434
526
|
</h3>
|
|
435
527
|
<p className="text-sm text-gray-500">Upload your first file to get started.</p>
|
|
436
528
|
</div>
|
|
437
529
|
) : (
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
</div>
|
|
470
|
-
{hasIssues && (
|
|
471
|
-
<div className="absolute top-1.5 left-1.5 w-5 h-5 bg-yellow-500 rounded-full flex items-center justify-center" title="Needs attention">
|
|
472
|
-
<AlertTriangle className="w-3 h-3 text-white" />
|
|
530
|
+
<div className="flex gap-4 flex-1 overflow-hidden min-h-0">
|
|
531
|
+
<div
|
|
532
|
+
className={`bg-white rounded-lg border border-gray-200 overflow-hidden transition-all duration-200 ${panelOpen ? 'flex-1 min-w-0' : 'w-full'}`}
|
|
533
|
+
>
|
|
534
|
+
{viewMode === 'grid' ? (
|
|
535
|
+
<div
|
|
536
|
+
className={`grid gap-2 sm:gap-3 p-2 sm:p-3 overflow-y-auto h-full ${
|
|
537
|
+
panelOpen
|
|
538
|
+
? 'grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'
|
|
539
|
+
: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-8'
|
|
540
|
+
}`}
|
|
541
|
+
>
|
|
542
|
+
{filteredAndSorted.map((item) => {
|
|
543
|
+
const isActive = activeItem?.id === item.id
|
|
544
|
+
const hasIssues = !item.altTag || !item.title || item.usedOn?.length === 0
|
|
545
|
+
return (
|
|
546
|
+
<div
|
|
547
|
+
key={item.id}
|
|
548
|
+
className={`group relative aspect-square rounded-lg border-2 overflow-hidden cursor-pointer transition-all ${
|
|
549
|
+
isActive
|
|
550
|
+
? 'border-blue-500 ring-2 ring-blue-200'
|
|
551
|
+
: selectedMedia.includes(item.id)
|
|
552
|
+
? 'border-blue-400 ring-1 ring-blue-100'
|
|
553
|
+
: 'border-gray-200 hover:border-gray-300'
|
|
554
|
+
}`}
|
|
555
|
+
onClick={() => openDetail(item)}
|
|
556
|
+
draggable
|
|
557
|
+
onDragStart={(e) => handleDragStart(e, item.id)}
|
|
558
|
+
>
|
|
559
|
+
<div className="w-full h-full bg-gray-100 flex items-center justify-center">
|
|
560
|
+
<MediaPreview item={item} />
|
|
473
561
|
</div>
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
{selectedMedia.includes(item.id) && (
|
|
480
|
-
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 12 12">
|
|
481
|
-
<path d="M10 3L4.5 8.5L2 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
|
482
|
-
</svg>
|
|
483
|
-
)}
|
|
562
|
+
<div className="absolute inset-0 bg-linear-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity">
|
|
563
|
+
<div className="absolute bottom-0 left-0 right-0 p-2">
|
|
564
|
+
<p className="text-white text-xs font-medium truncate">{item.name}</p>
|
|
565
|
+
<p className="text-white/80 text-xs">{item.size}</p>
|
|
566
|
+
</div>
|
|
484
567
|
</div>
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
<th className="w-6 px-1 py-2"></th>
|
|
497
|
-
<th className="px-3 py-2 text-left"><SortHeader label="Name" sortKey="name" /></th>
|
|
498
|
-
<th className="px-3 py-2 text-left"><SortHeader label="Type" sortKey="type" /></th>
|
|
499
|
-
<th className="px-3 py-2 text-left"><SortHeader label="Size" sortKey="size" /></th>
|
|
500
|
-
<th className="px-3 py-2 text-left"><SortHeader label="Uploaded" sortKey="date" /></th>
|
|
501
|
-
<th className="px-3 py-2 text-left text-xs font-medium text-gray-700">Status</th>
|
|
502
|
-
</tr>
|
|
503
|
-
</thead>
|
|
504
|
-
<tbody className="divide-y divide-gray-200">
|
|
505
|
-
{filteredAndSorted.map((item) => {
|
|
506
|
-
const isActive = activeItem?.id === item.id;
|
|
507
|
-
const hasIssues = !item.altTag || !item.title || item.usedOn?.length === 0;
|
|
508
|
-
return (
|
|
509
|
-
<tr
|
|
510
|
-
key={item.id}
|
|
511
|
-
className={`transition-colors cursor-pointer ${isActive ? 'bg-blue-50' : 'hover:bg-gray-50'}`}
|
|
512
|
-
onClick={() => openDetail(item)}
|
|
513
|
-
draggable
|
|
514
|
-
onDragStart={(e) => handleDragStart(e, item.id)}
|
|
568
|
+
{hasIssues && (
|
|
569
|
+
<div
|
|
570
|
+
className="absolute top-1.5 left-1.5 w-5 h-5 bg-yellow-500 rounded-full flex items-center justify-center"
|
|
571
|
+
title="Needs attention"
|
|
572
|
+
>
|
|
573
|
+
<AlertTriangle className="w-3 h-3 text-white" />
|
|
574
|
+
</div>
|
|
575
|
+
)}
|
|
576
|
+
<div
|
|
577
|
+
className="absolute top-1.5 right-1.5 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
578
|
+
onClick={(e) => handleCheckbox(e, item.id)}
|
|
515
579
|
>
|
|
516
|
-
<
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
<
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
|
534
|
-
<AlertTriangle className="w-3 h-3" /> Needs attention
|
|
535
|
-
</span>
|
|
536
|
-
) : (
|
|
537
|
-
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">Complete</span>
|
|
580
|
+
<div
|
|
581
|
+
className={`w-5 h-5 rounded border-2 flex items-center justify-center transition-colors ${
|
|
582
|
+
selectedMedia.includes(item.id)
|
|
583
|
+
? 'bg-blue-600 border-blue-600'
|
|
584
|
+
: 'bg-white/80 border-gray-400'
|
|
585
|
+
}`}
|
|
586
|
+
>
|
|
587
|
+
{selectedMedia.includes(item.id) && (
|
|
588
|
+
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 12 12">
|
|
589
|
+
<path
|
|
590
|
+
d="M10 3L4.5 8.5L2 6"
|
|
591
|
+
stroke="currentColor"
|
|
592
|
+
strokeWidth="2"
|
|
593
|
+
strokeLinecap="round"
|
|
594
|
+
strokeLinejoin="round"
|
|
595
|
+
/>
|
|
596
|
+
</svg>
|
|
538
597
|
)}
|
|
539
|
-
</
|
|
540
|
-
</
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
</
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
598
|
+
</div>
|
|
599
|
+
</div>
|
|
600
|
+
</div>
|
|
601
|
+
)
|
|
602
|
+
})}
|
|
603
|
+
</div>
|
|
604
|
+
) : (
|
|
605
|
+
<div className="overflow-y-auto h-full">
|
|
606
|
+
<table className="w-full">
|
|
607
|
+
<thead className="bg-gray-50 border-b border-gray-200 sticky top-0">
|
|
608
|
+
<tr>
|
|
609
|
+
<th className="w-8 px-3 py-2 text-left">
|
|
610
|
+
<input
|
|
611
|
+
type="checkbox"
|
|
612
|
+
checked={
|
|
613
|
+
selectedMedia.length === filteredAndSorted.length &&
|
|
614
|
+
filteredAndSorted.length > 0
|
|
615
|
+
}
|
|
616
|
+
onChange={handleSelectAll}
|
|
617
|
+
className="rounded border-gray-300"
|
|
618
|
+
/>
|
|
619
|
+
</th>
|
|
620
|
+
<th className="w-6 px-1 py-2"></th>
|
|
621
|
+
<th className="px-3 py-2 text-left">
|
|
622
|
+
<SortHeader label="Name" sortKey="name" />
|
|
623
|
+
</th>
|
|
624
|
+
<th className="px-3 py-2 text-left">
|
|
625
|
+
<SortHeader label="Type" sortKey="type" />
|
|
626
|
+
</th>
|
|
627
|
+
<th className="px-3 py-2 text-left">
|
|
628
|
+
<SortHeader label="Size" sortKey="size" />
|
|
629
|
+
</th>
|
|
630
|
+
<th className="px-3 py-2 text-left">
|
|
631
|
+
<SortHeader label="Uploaded" sortKey="date" />
|
|
632
|
+
</th>
|
|
633
|
+
<th className="px-3 py-2 text-left text-xs font-medium text-gray-700">
|
|
634
|
+
Status
|
|
635
|
+
</th>
|
|
636
|
+
</tr>
|
|
637
|
+
</thead>
|
|
638
|
+
<tbody className="divide-y divide-gray-200">
|
|
639
|
+
{filteredAndSorted.map((item) => {
|
|
640
|
+
const isActive = activeItem?.id === item.id
|
|
641
|
+
const hasIssues = !item.altTag || !item.title || item.usedOn?.length === 0
|
|
642
|
+
return (
|
|
643
|
+
<tr
|
|
644
|
+
key={item.id}
|
|
645
|
+
className={`transition-colors cursor-pointer ${isActive ? 'bg-blue-50' : 'hover:bg-gray-50'}`}
|
|
646
|
+
onClick={() => openDetail(item)}
|
|
647
|
+
draggable
|
|
648
|
+
onDragStart={(e) => handleDragStart(e, item.id)}
|
|
649
|
+
>
|
|
650
|
+
<td className="px-3 py-2" onClick={(e) => e.stopPropagation()}>
|
|
651
|
+
<input
|
|
652
|
+
type="checkbox"
|
|
653
|
+
checked={selectedMedia.includes(item.id)}
|
|
654
|
+
onChange={() =>
|
|
655
|
+
handleCheckbox(
|
|
656
|
+
{ stopPropagation: () => {} } as React.MouseEvent,
|
|
657
|
+
item.id,
|
|
658
|
+
)
|
|
659
|
+
}
|
|
660
|
+
className="rounded border-gray-300"
|
|
661
|
+
/>
|
|
662
|
+
</td>
|
|
663
|
+
<td className="px-1 py-2 cursor-grab">
|
|
664
|
+
<GripVertical className="w-4 h-4 text-gray-300" />
|
|
665
|
+
</td>
|
|
666
|
+
<td className="px-3 py-2">
|
|
667
|
+
<div className="flex items-center gap-3">
|
|
668
|
+
<div className="w-10 h-10 bg-gray-100 rounded flex items-center justify-center overflow-hidden">
|
|
669
|
+
<MediaPreview item={item} />
|
|
670
|
+
</div>
|
|
671
|
+
<span className="text-sm font-medium text-gray-900">
|
|
672
|
+
{item.name}
|
|
673
|
+
</span>
|
|
674
|
+
</div>
|
|
675
|
+
</td>
|
|
676
|
+
<td className="px-3 py-2 text-sm text-gray-600">
|
|
677
|
+
{item.format ?? item.type}
|
|
678
|
+
</td>
|
|
679
|
+
<td className="px-3 py-2 text-sm text-gray-600">{item.size}</td>
|
|
680
|
+
<td className="px-3 py-2 text-sm text-gray-600">{item.date}</td>
|
|
681
|
+
<td className="px-3 py-2">
|
|
682
|
+
{hasIssues ? (
|
|
683
|
+
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
|
684
|
+
<AlertTriangle className="w-3 h-3" /> Needs attention
|
|
685
|
+
</span>
|
|
686
|
+
) : (
|
|
687
|
+
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
688
|
+
Complete
|
|
689
|
+
</span>
|
|
690
|
+
)}
|
|
691
|
+
</td>
|
|
692
|
+
</tr>
|
|
693
|
+
)
|
|
694
|
+
})}
|
|
695
|
+
</tbody>
|
|
696
|
+
</table>
|
|
697
|
+
</div>
|
|
698
|
+
)}
|
|
699
|
+
</div>
|
|
548
700
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
701
|
+
{panelOpen && activeItem && (
|
|
702
|
+
<div className="w-80 lg:w-96 bg-white rounded-lg border border-gray-200 overflow-y-auto shrink-0 flex flex-col">
|
|
703
|
+
<div className="flex items-center justify-between p-4 border-b border-gray-200 sticky top-0 bg-white z-10">
|
|
704
|
+
<h3 className="text-sm font-semibold text-gray-900 truncate">
|
|
705
|
+
{activeItem.name}
|
|
706
|
+
</h3>
|
|
707
|
+
<button
|
|
708
|
+
onClick={closeDetail}
|
|
709
|
+
className="p-1 hover:bg-gray-100 rounded transition-colors"
|
|
710
|
+
aria-label="Close panel"
|
|
711
|
+
>
|
|
712
|
+
<X className="w-4 h-4 text-gray-500" />
|
|
713
|
+
</button>
|
|
714
|
+
</div>
|
|
557
715
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
716
|
+
<div className="flex-1 overflow-y-auto">
|
|
717
|
+
<div className="p-4 border-b border-gray-200">
|
|
718
|
+
<div className="aspect-video bg-gray-100 rounded-lg flex items-center justify-center overflow-hidden">
|
|
719
|
+
{isImageMedia(activeItem) ? (
|
|
720
|
+
<MediaPreview item={activeItem} />
|
|
721
|
+
) : (
|
|
722
|
+
<ImageIcon className="w-12 h-12 text-gray-300" />
|
|
723
|
+
)}
|
|
724
|
+
</div>
|
|
566
725
|
</div>
|
|
567
|
-
</div>
|
|
568
726
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
727
|
+
{issues.length > 0 && (
|
|
728
|
+
<div className="mx-4 mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
|
729
|
+
<div className="flex items-start gap-2">
|
|
730
|
+
<AlertTriangle className="w-4 h-4 text-yellow-600 mt-0.5 shrink-0" />
|
|
731
|
+
<div>
|
|
732
|
+
<p className="text-xs font-semibold text-yellow-900 mb-1">
|
|
733
|
+
{issues.length} issue{issues.length !== 1 ? 's' : ''} found
|
|
734
|
+
</p>
|
|
735
|
+
<ul className="space-y-0.5">
|
|
736
|
+
{issues.map((issue, i) => (
|
|
737
|
+
<li key={i} className="text-xs text-yellow-800">
|
|
738
|
+
• {issue}
|
|
739
|
+
</li>
|
|
740
|
+
))}
|
|
741
|
+
</ul>
|
|
742
|
+
</div>
|
|
743
|
+
</div>
|
|
744
|
+
<button
|
|
745
|
+
type="button"
|
|
746
|
+
onClick={async () => {
|
|
747
|
+
if (!activeItem.altTag) await handleAiGenerate('alt')
|
|
748
|
+
if (!activeItem.title) await handleAiGenerate('title')
|
|
749
|
+
if (activeItem.sizeBytes > 2000000) await handleAiGenerate('optimize')
|
|
750
|
+
}}
|
|
751
|
+
className="mt-2 w-full flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 transition-colors"
|
|
752
|
+
>
|
|
753
|
+
<Sparkles className="w-3.5 h-3.5" />
|
|
754
|
+
AI Fix All Issues
|
|
755
|
+
</button>
|
|
756
|
+
</div>
|
|
757
|
+
)}
|
|
758
|
+
|
|
759
|
+
<div className="p-4 border-b border-gray-200 space-y-3">
|
|
760
|
+
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
|
761
|
+
File Information
|
|
762
|
+
</h4>
|
|
763
|
+
<div className="grid grid-cols-2 gap-3">
|
|
573
764
|
<div>
|
|
574
|
-
<
|
|
575
|
-
<
|
|
576
|
-
{
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
765
|
+
<div className="text-xs text-gray-500 mb-0.5">Format</div>
|
|
766
|
+
<div className="text-sm text-gray-900">
|
|
767
|
+
{activeItem.format ?? 'Unknown'}
|
|
768
|
+
</div>
|
|
769
|
+
</div>
|
|
770
|
+
<div>
|
|
771
|
+
<div className="text-xs text-gray-500 mb-0.5">File Size</div>
|
|
772
|
+
<div className="text-sm text-gray-900 flex items-center gap-1">
|
|
773
|
+
{activeItem.size}
|
|
774
|
+
{activeItem.sizeBytes > 2000000 && (
|
|
775
|
+
<span className="text-yellow-600 text-xs">(large)</span>
|
|
776
|
+
)}
|
|
777
|
+
</div>
|
|
778
|
+
</div>
|
|
779
|
+
<div>
|
|
780
|
+
<div className="text-xs text-gray-500 mb-0.5">Dimensions</div>
|
|
781
|
+
<div className="text-sm text-gray-900">
|
|
782
|
+
{activeItem.dimensions ?? '—'}
|
|
783
|
+
</div>
|
|
784
|
+
</div>
|
|
785
|
+
<div>
|
|
786
|
+
<div className="text-xs text-gray-500 mb-0.5">Uploaded</div>
|
|
787
|
+
<div className="text-sm text-gray-900">{activeItem.date}</div>
|
|
580
788
|
</div>
|
|
581
789
|
</div>
|
|
582
|
-
<button
|
|
583
|
-
type="button"
|
|
584
|
-
onClick={async () => {
|
|
585
|
-
if (!activeItem.altTag) await handleAiGenerate('alt');
|
|
586
|
-
if (!activeItem.title) await handleAiGenerate('title');
|
|
587
|
-
if (activeItem.sizeBytes > 2000000) await handleAiGenerate('optimize');
|
|
588
|
-
}}
|
|
589
|
-
className="mt-2 w-full flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 transition-colors"
|
|
590
|
-
>
|
|
591
|
-
<Sparkles className="w-3.5 h-3.5" />
|
|
592
|
-
AI Fix All Issues
|
|
593
|
-
</button>
|
|
594
|
-
</div>
|
|
595
|
-
)}
|
|
596
790
|
|
|
597
|
-
<div className="p-4 border-b border-gray-200 space-y-3">
|
|
598
|
-
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide">File Information</h4>
|
|
599
|
-
<div className="grid grid-cols-2 gap-3">
|
|
600
791
|
<div>
|
|
601
|
-
<div className="text-xs text-gray-500 mb-
|
|
602
|
-
<div className="
|
|
792
|
+
<div className="text-xs text-gray-500 mb-1">URL</div>
|
|
793
|
+
<div className="flex items-center gap-1">
|
|
794
|
+
<code className="flex-1 text-xs bg-gray-50 border border-gray-200 px-2 py-1.5 rounded text-gray-700 truncate">
|
|
795
|
+
{activeItem.url}
|
|
796
|
+
</code>
|
|
797
|
+
<button
|
|
798
|
+
onClick={handleCopyUrl}
|
|
799
|
+
className="p-1.5 hover:bg-gray-100 rounded transition-colors shrink-0"
|
|
800
|
+
title="Copy URL"
|
|
801
|
+
>
|
|
802
|
+
<Copy className="w-3.5 h-3.5 text-gray-500" />
|
|
803
|
+
</button>
|
|
804
|
+
</div>
|
|
603
805
|
</div>
|
|
806
|
+
</div>
|
|
807
|
+
|
|
808
|
+
<div className="p-4 border-b border-gray-200 space-y-4">
|
|
809
|
+
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
|
810
|
+
SEO & Accessibility
|
|
811
|
+
</h4>
|
|
812
|
+
|
|
604
813
|
<div>
|
|
605
|
-
<div className="
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
814
|
+
<div className="flex items-center justify-between mb-1">
|
|
815
|
+
<label className="text-sm font-medium text-gray-700">Alt Tag</label>
|
|
816
|
+
<button
|
|
817
|
+
type="button"
|
|
818
|
+
onClick={() => handleAiGenerate('alt')}
|
|
819
|
+
disabled={aiGenerating === 'alt'}
|
|
820
|
+
className="flex items-center gap-1 text-xs text-indigo-600 hover:text-indigo-700 disabled:opacity-50 transition-colors"
|
|
821
|
+
>
|
|
822
|
+
<Bot className="w-3.5 h-3.5" />
|
|
823
|
+
{aiGenerating === 'alt' ? 'Generating...' : 'AI Generate'}
|
|
824
|
+
</button>
|
|
611
825
|
</div>
|
|
826
|
+
<textarea
|
|
827
|
+
value={editAlt}
|
|
828
|
+
onChange={(e) => setEditAlt(e.target.value)}
|
|
829
|
+
placeholder="Describe this image for accessibility..."
|
|
830
|
+
rows={2}
|
|
831
|
+
className="w-full text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
|
832
|
+
/>
|
|
833
|
+
{!editAlt && (
|
|
834
|
+
<p className="text-xs text-red-500 mt-1">
|
|
835
|
+
Required for accessibility and SEO
|
|
836
|
+
</p>
|
|
837
|
+
)}
|
|
612
838
|
</div>
|
|
839
|
+
|
|
613
840
|
<div>
|
|
614
|
-
<div className="
|
|
615
|
-
|
|
841
|
+
<div className="flex items-center justify-between mb-1">
|
|
842
|
+
<label className="text-sm font-medium text-gray-700">Title</label>
|
|
843
|
+
<button
|
|
844
|
+
type="button"
|
|
845
|
+
onClick={() => handleAiGenerate('title')}
|
|
846
|
+
disabled={aiGenerating === 'title'}
|
|
847
|
+
className="flex items-center gap-1 text-xs text-indigo-600 hover:text-indigo-700 disabled:opacity-50 transition-colors"
|
|
848
|
+
>
|
|
849
|
+
<Bot className="w-3.5 h-3.5" />
|
|
850
|
+
{aiGenerating === 'title' ? 'Generating...' : 'AI Generate'}
|
|
851
|
+
</button>
|
|
852
|
+
</div>
|
|
853
|
+
<input
|
|
854
|
+
type="text"
|
|
855
|
+
value={editTitle}
|
|
856
|
+
onChange={(e) => setEditTitle(e.target.value)}
|
|
857
|
+
placeholder="Image title..."
|
|
858
|
+
className="w-full text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
859
|
+
/>
|
|
616
860
|
</div>
|
|
861
|
+
|
|
617
862
|
<div>
|
|
618
|
-
<
|
|
619
|
-
|
|
863
|
+
<label className="text-sm font-medium text-gray-700 mb-1 block">
|
|
864
|
+
File Name
|
|
865
|
+
</label>
|
|
866
|
+
<input
|
|
867
|
+
type="text"
|
|
868
|
+
value={editFilename}
|
|
869
|
+
onChange={(e) => setEditFilename(e.target.value)}
|
|
870
|
+
className="w-full text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
871
|
+
/>
|
|
620
872
|
</div>
|
|
621
873
|
</div>
|
|
622
874
|
|
|
623
|
-
|
|
624
|
-
<div className="
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
875
|
+
{isImageMedia(activeItem) && activeItem.url && (
|
|
876
|
+
<div className="p-4 border-b border-gray-200">
|
|
877
|
+
<FocalPointPicker
|
|
878
|
+
imageUrl={activeItem.url}
|
|
879
|
+
focalX={focalX}
|
|
880
|
+
focalY={focalY}
|
|
881
|
+
onChange={(x, y) => {
|
|
882
|
+
setFocalX(x)
|
|
883
|
+
setFocalY(y)
|
|
884
|
+
}}
|
|
885
|
+
/>
|
|
630
886
|
</div>
|
|
631
|
-
|
|
632
|
-
</div>
|
|
633
|
-
|
|
634
|
-
<div className="p-4 border-b border-gray-200 space-y-4">
|
|
635
|
-
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide">SEO & Accessibility</h4>
|
|
887
|
+
)}
|
|
636
888
|
|
|
637
|
-
<div>
|
|
638
|
-
<
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
889
|
+
<div className="p-4 border-b border-gray-200">
|
|
890
|
+
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">
|
|
891
|
+
Used On {activeItem.usedOn && `(${activeItem.usedOn.length})`}
|
|
892
|
+
</h4>
|
|
893
|
+
{activeItem.usedOn && activeItem.usedOn.length > 0 ? (
|
|
894
|
+
<div className="space-y-2">
|
|
895
|
+
{activeItem.usedOn.map((usage, i) => (
|
|
896
|
+
<button
|
|
897
|
+
key={i}
|
|
898
|
+
type="button"
|
|
899
|
+
onClick={() => onNavigate?.(usage.path)}
|
|
900
|
+
className="w-full flex items-center gap-2 p-2 rounded-lg border border-gray-200 hover:bg-gray-50 transition-colors text-left"
|
|
901
|
+
>
|
|
902
|
+
<Link2 className="w-4 h-4 text-gray-400 shrink-0" />
|
|
903
|
+
<span className="text-sm text-gray-900 flex-1 truncate">
|
|
904
|
+
{usage.page}
|
|
905
|
+
</span>
|
|
906
|
+
<ExternalLink className="w-3.5 h-3.5 text-gray-400 shrink-0" />
|
|
907
|
+
</button>
|
|
908
|
+
))}
|
|
909
|
+
</div>
|
|
910
|
+
) : (
|
|
911
|
+
<div className="p-3 bg-orange-50 border border-orange-200 rounded-lg">
|
|
912
|
+
<div className="flex items-start gap-2">
|
|
913
|
+
<AlertTriangle className="w-4 h-4 text-orange-600 mt-0.5 shrink-0" />
|
|
914
|
+
<div>
|
|
915
|
+
<p className="text-xs font-medium text-orange-900">Orphaned media</p>
|
|
916
|
+
<p className="text-xs text-orange-700 mt-0.5">
|
|
917
|
+
This file isn't used on any page. Consider deleting it to save
|
|
918
|
+
storage.
|
|
919
|
+
</p>
|
|
920
|
+
</div>
|
|
921
|
+
</div>
|
|
922
|
+
</div>
|
|
659
923
|
)}
|
|
660
924
|
</div>
|
|
661
925
|
|
|
662
|
-
<div>
|
|
663
|
-
<
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
/>
|
|
682
|
-
</div>
|
|
683
|
-
|
|
684
|
-
<div>
|
|
685
|
-
<label className="text-sm font-medium text-gray-700 mb-1 block">File Name</label>
|
|
686
|
-
<input
|
|
687
|
-
type="text"
|
|
688
|
-
value={editFilename}
|
|
689
|
-
onChange={(e) => setEditFilename(e.target.value)}
|
|
690
|
-
className="w-full text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
691
|
-
/>
|
|
692
|
-
</div>
|
|
693
|
-
</div>
|
|
694
|
-
|
|
695
|
-
{isImageMedia(activeItem) && activeItem.url && (
|
|
696
|
-
<div className="p-4 border-b border-gray-200">
|
|
697
|
-
<FocalPointPicker
|
|
698
|
-
imageUrl={activeItem.url}
|
|
699
|
-
focalX={focalX}
|
|
700
|
-
focalY={focalY}
|
|
701
|
-
onChange={(x, y) => { setFocalX(x); setFocalY(y); }}
|
|
702
|
-
/>
|
|
703
|
-
</div>
|
|
704
|
-
)}
|
|
705
|
-
|
|
706
|
-
<div className="p-4 border-b border-gray-200">
|
|
707
|
-
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">
|
|
708
|
-
Used On {activeItem.usedOn && `(${activeItem.usedOn.length})`}
|
|
709
|
-
</h4>
|
|
710
|
-
{activeItem.usedOn && activeItem.usedOn.length > 0 ? (
|
|
711
|
-
<div className="space-y-2">
|
|
712
|
-
{activeItem.usedOn.map((usage, i) => (
|
|
713
|
-
<button
|
|
714
|
-
key={i}
|
|
715
|
-
type="button"
|
|
716
|
-
onClick={() => onNavigate?.(usage.path)}
|
|
717
|
-
className="w-full flex items-center gap-2 p-2 rounded-lg border border-gray-200 hover:bg-gray-50 transition-colors text-left"
|
|
718
|
-
>
|
|
719
|
-
<Link2 className="w-4 h-4 text-gray-400 shrink-0" />
|
|
720
|
-
<span className="text-sm text-gray-900 flex-1 truncate">{usage.page}</span>
|
|
721
|
-
<ExternalLink className="w-3.5 h-3.5 text-gray-400 shrink-0" />
|
|
722
|
-
</button>
|
|
723
|
-
))}
|
|
724
|
-
</div>
|
|
725
|
-
) : (
|
|
726
|
-
<div className="p-3 bg-orange-50 border border-orange-200 rounded-lg">
|
|
727
|
-
<div className="flex items-start gap-2">
|
|
728
|
-
<AlertTriangle className="w-4 h-4 text-orange-600 mt-0.5 shrink-0" />
|
|
729
|
-
<div>
|
|
730
|
-
<p className="text-xs font-medium text-orange-900">Orphaned media</p>
|
|
731
|
-
<p className="text-xs text-orange-700 mt-0.5">This file isn't used on any page. Consider deleting it to save storage.</p>
|
|
926
|
+
<div className="p-4 space-y-3">
|
|
927
|
+
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
|
928
|
+
AI Optimization
|
|
929
|
+
</h4>
|
|
930
|
+
<button
|
|
931
|
+
type="button"
|
|
932
|
+
onClick={() => handleAiGenerate('optimize')}
|
|
933
|
+
disabled={aiGenerating === 'optimize'}
|
|
934
|
+
className="w-full flex items-center gap-2 p-3 rounded-lg border border-indigo-200 bg-indigo-50 hover:bg-indigo-100 transition-colors text-left disabled:opacity-50"
|
|
935
|
+
>
|
|
936
|
+
<Sparkles
|
|
937
|
+
className={`w-5 h-5 text-indigo-600 shrink-0 ${aiGenerating === 'optimize' ? 'animate-spin' : ''}`}
|
|
938
|
+
/>
|
|
939
|
+
<div className="flex-1">
|
|
940
|
+
<div className="text-sm font-medium text-indigo-900">
|
|
941
|
+
{aiGenerating === 'optimize' ? 'Optimizing...' : 'Optimize Image'}
|
|
942
|
+
</div>
|
|
943
|
+
<div className="text-xs text-indigo-700 mt-0.5">
|
|
944
|
+
Compress and convert to modern format (WebP/AVIF)
|
|
732
945
|
</div>
|
|
733
946
|
</div>
|
|
734
|
-
</
|
|
735
|
-
|
|
947
|
+
</button>
|
|
948
|
+
<button
|
|
949
|
+
type="button"
|
|
950
|
+
className="w-full flex items-center gap-2 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 transition-colors text-left"
|
|
951
|
+
>
|
|
952
|
+
<Bot className="w-5 h-5 text-gray-500 shrink-0" />
|
|
953
|
+
<div className="flex-1">
|
|
954
|
+
<div className="text-sm font-medium text-gray-900">
|
|
955
|
+
AI Content Analysis
|
|
956
|
+
</div>
|
|
957
|
+
<div className="text-xs text-gray-600 mt-0.5">
|
|
958
|
+
Detect objects, faces, text, and suggest categories
|
|
959
|
+
</div>
|
|
960
|
+
</div>
|
|
961
|
+
</button>
|
|
962
|
+
</div>
|
|
736
963
|
</div>
|
|
737
964
|
|
|
738
|
-
<div className="p-4
|
|
739
|
-
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide">AI Optimization</h4>
|
|
965
|
+
<div className="p-4 border-t border-gray-200 bg-white sticky bottom-0 flex items-center gap-2">
|
|
740
966
|
<button
|
|
741
967
|
type="button"
|
|
742
|
-
onClick={
|
|
743
|
-
disabled={
|
|
744
|
-
className="
|
|
968
|
+
onClick={handleSaveDetails}
|
|
969
|
+
disabled={saving}
|
|
970
|
+
className="flex-1 px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
|
|
745
971
|
>
|
|
746
|
-
<
|
|
747
|
-
|
|
748
|
-
<div className="text-sm font-medium text-indigo-900">
|
|
749
|
-
{aiGenerating === 'optimize' ? 'Optimizing...' : 'Optimize Image'}
|
|
750
|
-
</div>
|
|
751
|
-
<div className="text-xs text-indigo-700 mt-0.5">
|
|
752
|
-
Compress and convert to modern format (WebP/AVIF)
|
|
753
|
-
</div>
|
|
754
|
-
</div>
|
|
972
|
+
{saving && <Loader2 className="w-4 h-4 animate-spin" />}
|
|
973
|
+
{saving ? 'Saving...' : 'Save Changes'}
|
|
755
974
|
</button>
|
|
756
975
|
<button
|
|
757
976
|
type="button"
|
|
758
|
-
className="
|
|
977
|
+
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
978
|
+
title="Download"
|
|
759
979
|
>
|
|
760
|
-
<
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
980
|
+
<Download className="w-4 h-4 text-gray-600" />
|
|
981
|
+
</button>
|
|
982
|
+
<button
|
|
983
|
+
type="button"
|
|
984
|
+
onClick={() => deleteMedia(activeItem.id)}
|
|
985
|
+
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
986
|
+
title="Delete"
|
|
987
|
+
>
|
|
988
|
+
<Trash2 className="w-4 h-4 text-red-600" />
|
|
767
989
|
</button>
|
|
768
990
|
</div>
|
|
769
991
|
</div>
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
<button
|
|
773
|
-
type="button"
|
|
774
|
-
onClick={handleSaveDetails}
|
|
775
|
-
disabled={saving}
|
|
776
|
-
className="flex-1 px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
|
|
777
|
-
>
|
|
778
|
-
{saving && <Loader2 className="w-4 h-4 animate-spin" />}
|
|
779
|
-
{saving ? 'Saving...' : 'Save Changes'}
|
|
780
|
-
</button>
|
|
781
|
-
<button
|
|
782
|
-
type="button"
|
|
783
|
-
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
784
|
-
title="Download"
|
|
785
|
-
>
|
|
786
|
-
<Download className="w-4 h-4 text-gray-600" />
|
|
787
|
-
</button>
|
|
788
|
-
<button
|
|
789
|
-
type="button"
|
|
790
|
-
onClick={() => deleteMedia(activeItem.id)}
|
|
791
|
-
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
792
|
-
title="Delete"
|
|
793
|
-
>
|
|
794
|
-
<Trash2 className="w-4 h-4 text-red-600" />
|
|
795
|
-
</button>
|
|
796
|
-
</div>
|
|
797
|
-
</div>
|
|
798
|
-
)}
|
|
799
|
-
</div>
|
|
992
|
+
)}
|
|
993
|
+
</div>
|
|
800
994
|
)}
|
|
801
995
|
</div>
|
|
802
996
|
</div>
|
|
803
997
|
</div>
|
|
804
|
-
)
|
|
998
|
+
)
|
|
805
999
|
}
|