@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
package/src/views/SEO.tsx
CHANGED
|
@@ -1,136 +1,196 @@
|
|
|
1
|
-
'use client'
|
|
1
|
+
'use client'
|
|
2
2
|
|
|
3
|
-
import * as Tabs from '@radix-ui/react-tabs'
|
|
3
|
+
import * as Tabs from '@radix-ui/react-tabs'
|
|
4
4
|
import {
|
|
5
|
-
Search,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
Search,
|
|
6
|
+
Globe,
|
|
7
|
+
FileCode2,
|
|
8
|
+
BarChart3,
|
|
9
|
+
AlertTriangle,
|
|
10
|
+
CheckCircle2,
|
|
11
|
+
XCircle,
|
|
12
|
+
ArrowUpRight,
|
|
13
|
+
RefreshCw,
|
|
14
|
+
BookOpen,
|
|
15
|
+
Link2,
|
|
16
|
+
Bot,
|
|
17
|
+
Plus,
|
|
18
|
+
Pencil,
|
|
19
|
+
Trash2,
|
|
20
|
+
ExternalLink,
|
|
21
|
+
ArrowRightLeft,
|
|
22
|
+
Copy,
|
|
23
|
+
ShieldCheck,
|
|
24
|
+
Loader2,
|
|
25
|
+
} from 'lucide-react'
|
|
26
|
+
import { useState, useMemo, type FormEvent } from 'react'
|
|
27
|
+
import { toast } from 'sonner'
|
|
28
|
+
import * as Dialog from '@radix-ui/react-dialog'
|
|
29
|
+
import { useApiData } from '../lib/useApiData.js'
|
|
30
|
+
import { cmsApi } from '../lib/api.js'
|
|
14
31
|
|
|
15
32
|
export interface SEOProps {
|
|
16
|
-
onNavigate?: (path: string) => void
|
|
17
|
-
initialTab?: string
|
|
33
|
+
onNavigate?: (path: string) => void
|
|
34
|
+
initialTab?: string
|
|
18
35
|
}
|
|
19
36
|
|
|
20
37
|
export function SEO({ onNavigate, initialTab = 'pages' }: SEOProps) {
|
|
21
|
-
const {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
const
|
|
38
|
+
const {
|
|
39
|
+
data: seoData,
|
|
40
|
+
loading: seoLoading,
|
|
41
|
+
error: seoError,
|
|
42
|
+
refetch: seoRefetch,
|
|
43
|
+
} = useApiData<any[]>('/seo/pages')
|
|
44
|
+
const {
|
|
45
|
+
data: redirectsData,
|
|
46
|
+
loading: redirectsLoading,
|
|
47
|
+
error: redirectsError,
|
|
48
|
+
refetch: redirectsRefetch,
|
|
49
|
+
} = useApiData<any[]>('/redirects')
|
|
50
|
+
const { data: linkHealthData, loading: linkHealthLoading } = useApiData<any[]>('/seo/link-health')
|
|
51
|
+
|
|
52
|
+
const [activeTab, setActiveTab] = useState(initialTab)
|
|
53
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
54
|
+
const [filterScore, setFilterScore] = useState<string>('all')
|
|
55
|
+
const [scanning, setScanning] = useState(false)
|
|
56
|
+
|
|
57
|
+
const [showAddRedirect, setShowAddRedirect] = useState(false)
|
|
58
|
+
const [newRedirect, setNewRedirect] = useState({
|
|
59
|
+
source: '',
|
|
60
|
+
destination: '',
|
|
61
|
+
type: '301' as '301' | '302',
|
|
62
|
+
})
|
|
63
|
+
const [redirectSearch, setRedirectSearch] = useState('')
|
|
64
|
+
|
|
65
|
+
const seoPages = seoData ?? []
|
|
66
|
+
const redirects = redirectsData ?? []
|
|
67
|
+
const linkHealth = linkHealthData ?? []
|
|
37
68
|
|
|
38
69
|
const handleTabChange = (tab: string) => {
|
|
39
|
-
setActiveTab(tab)
|
|
40
|
-
const path = tab === 'pages' ? '/seo' : `/seo/${tab}
|
|
41
|
-
onNavigate?.(path)
|
|
42
|
-
}
|
|
70
|
+
setActiveTab(tab)
|
|
71
|
+
const path = tab === 'pages' ? '/seo' : `/seo/${tab}`
|
|
72
|
+
onNavigate?.(path)
|
|
73
|
+
}
|
|
43
74
|
|
|
44
75
|
// --- Pages tab data ---
|
|
45
76
|
const filtered = seoPages
|
|
46
77
|
.filter((page: any) => {
|
|
47
|
-
const matchesSearch =
|
|
48
|
-
|
|
49
|
-
|
|
78
|
+
const matchesSearch =
|
|
79
|
+
(page.url ?? '').toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
80
|
+
(page.title ?? '').toLowerCase().includes(searchQuery.toLowerCase())
|
|
81
|
+
const matchesScore =
|
|
82
|
+
filterScore === 'all' ||
|
|
83
|
+
(filterScore === 'good' && page.score >= 80) ||
|
|
84
|
+
(filterScore === 'warning' && page.score >= 50 && page.score < 80) ||
|
|
85
|
+
(filterScore === 'critical' && page.score < 50)
|
|
86
|
+
return matchesSearch && matchesScore
|
|
50
87
|
})
|
|
51
|
-
.sort((a: any, b: any) => !searchQuery ? b.issues - a.issues : 0)
|
|
88
|
+
.sort((a: any, b: any) => (!searchQuery ? b.issues - a.issues : 0))
|
|
52
89
|
|
|
53
|
-
const totalIssues = seoPages.reduce((sum: number, p: any) => sum + p.issues, 0)
|
|
54
|
-
const avgScore =
|
|
55
|
-
|
|
56
|
-
|
|
90
|
+
const totalIssues = seoPages.reduce((sum: number, p: any) => sum + p.issues, 0)
|
|
91
|
+
const avgScore =
|
|
92
|
+
seoPages.length > 0
|
|
93
|
+
? Math.round(seoPages.reduce((sum: number, p: any) => sum + p.score, 0) / seoPages.length)
|
|
94
|
+
: 0
|
|
95
|
+
const missingMeta = seoPages.filter((p: any) => !p.metaTitle || !p.metaDescription).length
|
|
96
|
+
const missingSchema = seoPages.filter((p: any) => !p.schemaType).length
|
|
57
97
|
|
|
58
98
|
// --- Canonical data ---
|
|
59
|
-
const missingCanonical = seoPages.filter((p: any) => !p.canonical)
|
|
60
|
-
const allCanonicals = seoPages.filter((p: any) => p.canonical)
|
|
61
|
-
const canonicalDomains = [
|
|
99
|
+
const missingCanonical = seoPages.filter((p: any) => !p.canonical)
|
|
100
|
+
const allCanonicals = seoPages.filter((p: any) => p.canonical)
|
|
101
|
+
const canonicalDomains = [
|
|
102
|
+
...new Set(
|
|
103
|
+
allCanonicals
|
|
104
|
+
.map((p: any) => {
|
|
105
|
+
try {
|
|
106
|
+
return new URL(p.canonical).hostname
|
|
107
|
+
} catch {
|
|
108
|
+
return ''
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
.filter(Boolean),
|
|
112
|
+
),
|
|
113
|
+
]
|
|
62
114
|
|
|
63
115
|
// --- Redirects data ---
|
|
64
|
-
const filteredRedirects = redirects.filter(
|
|
65
|
-
(r
|
|
66
|
-
|
|
67
|
-
|
|
116
|
+
const filteredRedirects = redirects.filter(
|
|
117
|
+
(r: any) =>
|
|
118
|
+
(r.from ?? '').toLowerCase().includes(redirectSearch.toLowerCase()) ||
|
|
119
|
+
(r.to ?? '').toLowerCase().includes(redirectSearch.toLowerCase()),
|
|
120
|
+
)
|
|
68
121
|
|
|
69
122
|
const handleScan = async () => {
|
|
70
|
-
setScanning(true)
|
|
123
|
+
setScanning(true)
|
|
71
124
|
try {
|
|
72
|
-
const res = await cmsApi('/seo/scan', { method: 'POST' })
|
|
125
|
+
const res = await cmsApi('/seo/scan', { method: 'POST' })
|
|
73
126
|
if (res.error) {
|
|
74
|
-
toast.error(res.error)
|
|
127
|
+
toast.error(res.error)
|
|
75
128
|
} else {
|
|
76
|
-
const d = res.data as { total: number; pagesWithIssues: number; totalProblems: number }
|
|
77
|
-
toast.success(
|
|
78
|
-
|
|
129
|
+
const d = res.data as { total: number; pagesWithIssues: number; totalProblems: number }
|
|
130
|
+
toast.success(
|
|
131
|
+
`SEO scan complete — ${d.totalProblems} issues found across ${d.pagesWithIssues} of ${d.total} pages`,
|
|
132
|
+
)
|
|
133
|
+
seoRefetch()
|
|
79
134
|
}
|
|
80
135
|
} catch {
|
|
81
|
-
toast.error('SEO scan failed')
|
|
136
|
+
toast.error('SEO scan failed')
|
|
82
137
|
} finally {
|
|
83
|
-
setScanning(false)
|
|
138
|
+
setScanning(false)
|
|
84
139
|
}
|
|
85
|
-
}
|
|
140
|
+
}
|
|
86
141
|
|
|
87
142
|
const handleAddRedirect = async (e: FormEvent) => {
|
|
88
|
-
e.preventDefault()
|
|
143
|
+
e.preventDefault()
|
|
89
144
|
const res = await cmsApi('/redirects', {
|
|
90
145
|
method: 'POST',
|
|
91
|
-
body: JSON.stringify({
|
|
92
|
-
|
|
146
|
+
body: JSON.stringify({
|
|
147
|
+
from: newRedirect.source,
|
|
148
|
+
to: newRedirect.destination,
|
|
149
|
+
type: newRedirect.type,
|
|
150
|
+
}),
|
|
151
|
+
})
|
|
93
152
|
if (res.error) {
|
|
94
|
-
toast.error(res.error)
|
|
153
|
+
toast.error(res.error)
|
|
95
154
|
} else {
|
|
96
|
-
toast.success('Redirect added')
|
|
97
|
-
redirectsRefetch()
|
|
155
|
+
toast.success('Redirect added')
|
|
156
|
+
redirectsRefetch()
|
|
98
157
|
}
|
|
99
|
-
setShowAddRedirect(false)
|
|
100
|
-
setNewRedirect({ source: '', destination: '', type: '301' })
|
|
101
|
-
}
|
|
158
|
+
setShowAddRedirect(false)
|
|
159
|
+
setNewRedirect({ source: '', destination: '', type: '301' })
|
|
160
|
+
}
|
|
102
161
|
|
|
103
162
|
const handleDeleteRedirect = async (id: number) => {
|
|
104
|
-
const res = await cmsApi(`/redirects/${id}`, { method: 'DELETE' })
|
|
163
|
+
const res = await cmsApi(`/redirects/${id}`, { method: 'DELETE' })
|
|
105
164
|
if (res.error) {
|
|
106
|
-
toast.error(res.error)
|
|
165
|
+
toast.error(res.error)
|
|
107
166
|
} else {
|
|
108
|
-
toast.success('Redirect deleted')
|
|
109
|
-
redirectsRefetch()
|
|
167
|
+
toast.success('Redirect deleted')
|
|
168
|
+
redirectsRefetch()
|
|
110
169
|
}
|
|
111
|
-
}
|
|
170
|
+
}
|
|
112
171
|
|
|
113
172
|
function scoreBadge(score: number) {
|
|
114
|
-
if (score >= 80) return 'bg-green-100 text-green-800'
|
|
115
|
-
if (score >= 50) return 'bg-yellow-100 text-yellow-800'
|
|
116
|
-
return 'bg-red-100 text-red-800'
|
|
173
|
+
if (score >= 80) return 'bg-green-100 text-green-800'
|
|
174
|
+
if (score >= 50) return 'bg-yellow-100 text-yellow-800'
|
|
175
|
+
return 'bg-red-100 text-red-800'
|
|
117
176
|
}
|
|
118
177
|
function scoreIcon(score: number) {
|
|
119
|
-
if (score >= 80) return <CheckCircle2 className="w-4 h-4 text-green-600"
|
|
120
|
-
if (score >= 50) return <AlertTriangle className="w-4 h-4 text-yellow-600"
|
|
121
|
-
return <XCircle className="w-4 h-4 text-red-600"
|
|
178
|
+
if (score >= 80) return <CheckCircle2 className="w-4 h-4 text-green-600" />
|
|
179
|
+
if (score >= 50) return <AlertTriangle className="w-4 h-4 text-yellow-600" />
|
|
180
|
+
return <XCircle className="w-4 h-4 text-red-600" />
|
|
122
181
|
}
|
|
123
182
|
|
|
124
|
-
const tabClass =
|
|
183
|
+
const tabClass =
|
|
184
|
+
'px-4 py-2 text-sm font-medium text-gray-600 transition-colors hover:text-gray-900 data-[state=active]:border-b-2 data-[state=active]:border-blue-600 data-[state=active]:text-blue-600 shrink-0'
|
|
125
185
|
|
|
126
|
-
const isLoading = seoLoading || redirectsLoading || linkHealthLoading
|
|
186
|
+
const isLoading = seoLoading || redirectsLoading || linkHealthLoading
|
|
127
187
|
|
|
128
188
|
if (isLoading) {
|
|
129
189
|
return (
|
|
130
190
|
<div className="p-3 pr-6 sm:p-4 sm:pr-8 flex items-center justify-center h-64">
|
|
131
191
|
<Loader2 className="w-6 h-6 animate-spin text-blue-600" />
|
|
132
192
|
</div>
|
|
133
|
-
)
|
|
193
|
+
)
|
|
134
194
|
}
|
|
135
195
|
|
|
136
196
|
return (
|
|
@@ -139,16 +199,30 @@ export function SEO({ onNavigate, initialTab = 'pages' }: SEOProps) {
|
|
|
139
199
|
<div className="mb-4 flex items-center gap-3 rounded-lg border border-red-200 bg-red-50 p-3">
|
|
140
200
|
<AlertTriangle className="w-5 h-5 text-red-600 shrink-0" />
|
|
141
201
|
<span className="text-sm text-red-800 flex-1">{seoError || redirectsError}</span>
|
|
142
|
-
<button
|
|
202
|
+
<button
|
|
203
|
+
onClick={() => {
|
|
204
|
+
seoRefetch()
|
|
205
|
+
redirectsRefetch()
|
|
206
|
+
}}
|
|
207
|
+
className="px-3 py-1 text-sm text-red-700 border border-red-300 rounded-lg hover:bg-red-100 transition-colors"
|
|
208
|
+
>
|
|
209
|
+
Retry
|
|
210
|
+
</button>
|
|
143
211
|
</div>
|
|
144
212
|
)}
|
|
145
213
|
|
|
146
214
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between mb-4 gap-3">
|
|
147
215
|
<div>
|
|
148
216
|
<h1 className="text-xl sm:text-2xl font-semibold text-gray-900 mb-1">SEO & Redirects</h1>
|
|
149
|
-
<p className="text-sm text-gray-600">
|
|
217
|
+
<p className="text-sm text-gray-600">
|
|
218
|
+
Search optimization, redirects, canonicalization, and link health
|
|
219
|
+
</p>
|
|
150
220
|
</div>
|
|
151
|
-
<button
|
|
221
|
+
<button
|
|
222
|
+
onClick={handleScan}
|
|
223
|
+
disabled={scanning}
|
|
224
|
+
className="flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm disabled:opacity-60"
|
|
225
|
+
>
|
|
152
226
|
<RefreshCw className={`w-4 h-4 ${scanning ? 'animate-spin' : ''}`} />
|
|
153
227
|
{scanning ? 'Scanning...' : 'Run SEO Scan'}
|
|
154
228
|
</button>
|
|
@@ -157,16 +231,28 @@ export function SEO({ onNavigate, initialTab = 'pages' }: SEOProps) {
|
|
|
157
231
|
<Tabs.Root value={activeTab} onValueChange={handleTabChange}>
|
|
158
232
|
<Tabs.List className="mb-4 flex gap-1 border-b border-gray-200 overflow-x-auto">
|
|
159
233
|
<Tabs.Trigger value="pages" className={tabClass}>
|
|
160
|
-
<span className="flex items-center gap-1.5"
|
|
234
|
+
<span className="flex items-center gap-1.5">
|
|
235
|
+
<BarChart3 className="w-4 h-4" />
|
|
236
|
+
Pages
|
|
237
|
+
</span>
|
|
161
238
|
</Tabs.Trigger>
|
|
162
239
|
<Tabs.Trigger value="redirects" className={tabClass}>
|
|
163
|
-
<span className="flex items-center gap-1.5"
|
|
240
|
+
<span className="flex items-center gap-1.5">
|
|
241
|
+
<ArrowRightLeft className="w-4 h-4" />
|
|
242
|
+
Redirects
|
|
243
|
+
</span>
|
|
164
244
|
</Tabs.Trigger>
|
|
165
245
|
<Tabs.Trigger value="canonicals" className={tabClass}>
|
|
166
|
-
<span className="flex items-center gap-1.5"
|
|
246
|
+
<span className="flex items-center gap-1.5">
|
|
247
|
+
<Copy className="w-4 h-4" />
|
|
248
|
+
Canonicalization
|
|
249
|
+
</span>
|
|
167
250
|
</Tabs.Trigger>
|
|
168
251
|
<Tabs.Trigger value="links" className={tabClass}>
|
|
169
|
-
<span className="flex items-center gap-1.5"
|
|
252
|
+
<span className="flex items-center gap-1.5">
|
|
253
|
+
<Link2 className="w-4 h-4" />
|
|
254
|
+
Link Health
|
|
255
|
+
</span>
|
|
170
256
|
</Tabs.Trigger>
|
|
171
257
|
</Tabs.List>
|
|
172
258
|
|
|
@@ -174,22 +260,40 @@ export function SEO({ onNavigate, initialTab = 'pages' }: SEOProps) {
|
|
|
174
260
|
<Tabs.Content value="pages" className="flex flex-col flex-1 min-h-0">
|
|
175
261
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
|
|
176
262
|
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
|
177
|
-
<div className="flex items-center gap-2 mb-2"
|
|
263
|
+
<div className="flex items-center gap-2 mb-2">
|
|
264
|
+
<BarChart3 className="w-4 h-4 text-blue-600" />
|
|
265
|
+
<span className="text-xs font-medium text-gray-600">Avg SEO Score</span>
|
|
266
|
+
</div>
|
|
178
267
|
<div className="text-2xl font-semibold text-gray-900">{avgScore}</div>
|
|
179
|
-
<div
|
|
268
|
+
<div
|
|
269
|
+
className={`text-xs mt-1 ${avgScore >= 80 ? 'text-green-600' : avgScore >= 50 ? 'text-yellow-600' : 'text-red-600'}`}
|
|
270
|
+
>
|
|
271
|
+
{avgScore >= 80 ? 'Good' : avgScore >= 50 ? 'Needs improvement' : 'Critical'}
|
|
272
|
+
</div>
|
|
180
273
|
</div>
|
|
181
274
|
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
|
182
|
-
<div className="flex items-center gap-2 mb-2"
|
|
275
|
+
<div className="flex items-center gap-2 mb-2">
|
|
276
|
+
<AlertTriangle className="w-4 h-4 text-yellow-600" />
|
|
277
|
+
<span className="text-xs font-medium text-gray-600">Total Issues</span>
|
|
278
|
+
</div>
|
|
183
279
|
<div className="text-2xl font-semibold text-gray-900">{totalIssues}</div>
|
|
184
|
-
<div className="text-xs text-gray-500 mt-1">
|
|
280
|
+
<div className="text-xs text-gray-500 mt-1">
|
|
281
|
+
Across {seoPages.filter((p: any) => p.issues > 0).length} pages
|
|
282
|
+
</div>
|
|
185
283
|
</div>
|
|
186
284
|
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
|
187
|
-
<div className="flex items-center gap-2 mb-2"
|
|
285
|
+
<div className="flex items-center gap-2 mb-2">
|
|
286
|
+
<FileCode2 className="w-4 h-4 text-purple-600" />
|
|
287
|
+
<span className="text-xs font-medium text-gray-600">Missing Meta</span>
|
|
288
|
+
</div>
|
|
188
289
|
<div className="text-2xl font-semibold text-gray-900">{missingMeta}</div>
|
|
189
290
|
<div className="text-xs text-gray-500 mt-1">Title or description</div>
|
|
190
291
|
</div>
|
|
191
292
|
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
|
192
|
-
<div className="flex items-center gap-2 mb-2"
|
|
293
|
+
<div className="flex items-center gap-2 mb-2">
|
|
294
|
+
<Globe className="w-4 h-4 text-green-600" />
|
|
295
|
+
<span className="text-xs font-medium text-gray-600">Missing Schema</span>
|
|
296
|
+
</div>
|
|
193
297
|
<div className="text-2xl font-semibold text-gray-900">{missingSchema}</div>
|
|
194
298
|
<div className="text-xs text-gray-500 mt-1">No Schema.org markup</div>
|
|
195
299
|
</div>
|
|
@@ -199,11 +303,23 @@ export function SEO({ onNavigate, initialTab = 'pages' }: SEOProps) {
|
|
|
199
303
|
<div className="flex items-start gap-3">
|
|
200
304
|
<Bot className="w-5 h-5 text-indigo-600 mt-0.5 shrink-0" />
|
|
201
305
|
<div className="flex-1">
|
|
202
|
-
<h3 className="text-sm font-semibold text-indigo-900 mb-1">
|
|
203
|
-
|
|
306
|
+
<h3 className="text-sm font-semibold text-indigo-900 mb-1">
|
|
307
|
+
AI SEO Recommendations
|
|
308
|
+
</h3>
|
|
309
|
+
<p className="text-sm text-indigo-700 mb-2">
|
|
310
|
+
{missingMeta} pages missing meta descriptions. {missingCanonical.length} pages
|
|
311
|
+
without canonical URLs. AI can auto-generate these from page content.
|
|
312
|
+
</p>
|
|
204
313
|
<div className="flex items-center gap-2">
|
|
205
|
-
<button
|
|
206
|
-
|
|
314
|
+
<button
|
|
315
|
+
onClick={() => onNavigate?.('/settings')}
|
|
316
|
+
className="px-3 py-1.5 text-xs bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
|
|
317
|
+
>
|
|
318
|
+
Configure AI
|
|
319
|
+
</button>
|
|
320
|
+
<button className="px-3 py-1.5 text-xs border border-indigo-300 text-indigo-700 rounded-lg hover:bg-indigo-50 transition-colors">
|
|
321
|
+
Auto-fix All
|
|
322
|
+
</button>
|
|
207
323
|
</div>
|
|
208
324
|
</div>
|
|
209
325
|
</div>
|
|
@@ -213,9 +329,19 @@ export function SEO({ onNavigate, initialTab = 'pages' }: SEOProps) {
|
|
|
213
329
|
<div className="p-3 flex flex-col sm:flex-row gap-3">
|
|
214
330
|
<div className="relative flex-1">
|
|
215
331
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
|
216
|
-
<input
|
|
332
|
+
<input
|
|
333
|
+
type="text"
|
|
334
|
+
placeholder="Search pages by URL or title..."
|
|
335
|
+
value={searchQuery}
|
|
336
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
337
|
+
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"
|
|
338
|
+
/>
|
|
217
339
|
</div>
|
|
218
|
-
<select
|
|
340
|
+
<select
|
|
341
|
+
value={filterScore}
|
|
342
|
+
onChange={(e) => setFilterScore(e.target.value)}
|
|
343
|
+
className="px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
344
|
+
>
|
|
219
345
|
<option value="all">All Scores</option>
|
|
220
346
|
<option value="good">Good (80+)</option>
|
|
221
347
|
<option value="warning">Needs Work (50-79)</option>
|
|
@@ -231,23 +357,105 @@ export function SEO({ onNavigate, initialTab = 'pages' }: SEOProps) {
|
|
|
231
357
|
<tr>
|
|
232
358
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">Page</th>
|
|
233
359
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">Score</th>
|
|
234
|
-
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
235
|
-
|
|
360
|
+
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
361
|
+
Readability
|
|
362
|
+
</th>
|
|
363
|
+
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
364
|
+
Schema
|
|
365
|
+
</th>
|
|
236
366
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">Meta</th>
|
|
237
|
-
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
238
|
-
|
|
367
|
+
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
368
|
+
Issues
|
|
369
|
+
</th>
|
|
370
|
+
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
371
|
+
Actions
|
|
372
|
+
</th>
|
|
239
373
|
</tr>
|
|
240
374
|
</thead>
|
|
241
375
|
<tbody className="divide-y divide-gray-200">
|
|
242
376
|
{filtered.map((page: any) => (
|
|
243
377
|
<tr key={page.id} className="hover:bg-gray-50 transition-colors">
|
|
244
|
-
<td className="px-4 py-3"
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
<td className="px-4 py-3"
|
|
249
|
-
|
|
250
|
-
|
|
378
|
+
<td className="px-4 py-3">
|
|
379
|
+
<div className="text-sm font-medium text-gray-900">{page.title}</div>
|
|
380
|
+
<div className="text-xs text-gray-500">{page.url}</div>
|
|
381
|
+
</td>
|
|
382
|
+
<td className="px-4 py-3">
|
|
383
|
+
<div className="flex items-center gap-2">
|
|
384
|
+
{scoreIcon(page.score)}
|
|
385
|
+
<span
|
|
386
|
+
className={`px-2 py-0.5 rounded-full text-xs font-medium ${scoreBadge(page.score)}`}
|
|
387
|
+
>
|
|
388
|
+
{page.score}
|
|
389
|
+
</span>
|
|
390
|
+
</div>
|
|
391
|
+
</td>
|
|
392
|
+
<td className="px-4 py-3">
|
|
393
|
+
{page.readability > 0 ? (
|
|
394
|
+
<div className="flex items-center gap-2">
|
|
395
|
+
<BookOpen className="w-3.5 h-3.5 text-gray-400" />
|
|
396
|
+
<span
|
|
397
|
+
className={`text-sm ${page.readability >= 80 ? 'text-green-700' : page.readability >= 60 ? 'text-yellow-700' : 'text-red-700'}`}
|
|
398
|
+
>
|
|
399
|
+
{page.readability}
|
|
400
|
+
</span>
|
|
401
|
+
</div>
|
|
402
|
+
) : (
|
|
403
|
+
<span className="text-xs text-gray-400">—</span>
|
|
404
|
+
)}
|
|
405
|
+
</td>
|
|
406
|
+
<td className="px-4 py-3">
|
|
407
|
+
{page.schemaType ? (
|
|
408
|
+
<span className="px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
|
|
409
|
+
{page.schemaType}
|
|
410
|
+
</span>
|
|
411
|
+
) : (
|
|
412
|
+
<span className="px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
|
413
|
+
Missing
|
|
414
|
+
</span>
|
|
415
|
+
)}
|
|
416
|
+
</td>
|
|
417
|
+
<td className="px-4 py-3">
|
|
418
|
+
<div className="flex flex-col gap-0.5">
|
|
419
|
+
<span
|
|
420
|
+
className={`text-xs ${page.metaTitle ? 'text-green-700' : 'text-red-600'}`}
|
|
421
|
+
>
|
|
422
|
+
{page.metaTitle ? '✓ Title' : '✗ Title'}
|
|
423
|
+
</span>
|
|
424
|
+
<span
|
|
425
|
+
className={`text-xs ${page.metaDescription ? 'text-green-700' : 'text-red-600'}`}
|
|
426
|
+
>
|
|
427
|
+
{page.metaDescription ? '✓ Desc' : '✗ Desc'}
|
|
428
|
+
</span>
|
|
429
|
+
</div>
|
|
430
|
+
</td>
|
|
431
|
+
<td className="px-4 py-3">
|
|
432
|
+
{page.issues > 0 ? (
|
|
433
|
+
<span className="px-2 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
|
434
|
+
{page.issues}
|
|
435
|
+
</span>
|
|
436
|
+
) : (
|
|
437
|
+
<span className="px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
438
|
+
✓
|
|
439
|
+
</span>
|
|
440
|
+
)}
|
|
441
|
+
</td>
|
|
442
|
+
<td className="px-4 py-3">
|
|
443
|
+
<div className="flex items-center gap-1">
|
|
444
|
+
<button
|
|
445
|
+
onClick={() => onNavigate?.(`/pages/${page.id}`)}
|
|
446
|
+
className="p-1.5 hover:bg-gray-100 rounded transition-colors"
|
|
447
|
+
title="Edit"
|
|
448
|
+
>
|
|
449
|
+
<ArrowUpRight className="w-4 h-4 text-gray-600" />
|
|
450
|
+
</button>
|
|
451
|
+
<button
|
|
452
|
+
className="p-1.5 hover:bg-gray-100 rounded transition-colors"
|
|
453
|
+
title="AI analyze"
|
|
454
|
+
>
|
|
455
|
+
<Bot className="w-4 h-4 text-indigo-600" />
|
|
456
|
+
</button>
|
|
457
|
+
</div>
|
|
458
|
+
</td>
|
|
251
459
|
</tr>
|
|
252
460
|
))}
|
|
253
461
|
</tbody>
|
|
@@ -265,25 +473,41 @@ export function SEO({ onNavigate, initialTab = 'pages' }: SEOProps) {
|
|
|
265
473
|
</div>
|
|
266
474
|
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
|
267
475
|
<div className="text-xs text-gray-600 mb-1">Active</div>
|
|
268
|
-
<div className="text-2xl font-semibold text-green-600">
|
|
476
|
+
<div className="text-2xl font-semibold text-green-600">
|
|
477
|
+
{redirects.filter((r: any) => r.active).length}
|
|
478
|
+
</div>
|
|
269
479
|
</div>
|
|
270
480
|
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
|
271
481
|
<div className="text-xs text-gray-600 mb-1">Total Hits</div>
|
|
272
|
-
<div className="text-2xl font-semibold text-blue-600">
|
|
482
|
+
<div className="text-2xl font-semibold text-blue-600">
|
|
483
|
+
{redirects.reduce((s: number, r: any) => s + r.hits, 0).toLocaleString()}
|
|
484
|
+
</div>
|
|
273
485
|
</div>
|
|
274
486
|
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
|
275
487
|
<div className="text-xs text-gray-600 mb-1">301 Permanent</div>
|
|
276
|
-
<div className="text-2xl font-semibold text-gray-900">
|
|
488
|
+
<div className="text-2xl font-semibold text-gray-900">
|
|
489
|
+
{redirects.filter((r: any) => r.type === '301').length}
|
|
490
|
+
</div>
|
|
277
491
|
</div>
|
|
278
492
|
</div>
|
|
279
493
|
|
|
280
494
|
<div className="flex items-center gap-3 mb-4">
|
|
281
495
|
<div className="relative flex-1 max-w-md">
|
|
282
496
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
|
283
|
-
<input
|
|
497
|
+
<input
|
|
498
|
+
type="text"
|
|
499
|
+
placeholder="Search redirects..."
|
|
500
|
+
value={redirectSearch}
|
|
501
|
+
onChange={(e) => setRedirectSearch(e.target.value)}
|
|
502
|
+
className="w-full pl-9 pr-3 py-2 text-sm border border-gray-300 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
503
|
+
/>
|
|
284
504
|
</div>
|
|
285
|
-
<button
|
|
286
|
-
|
|
505
|
+
<button
|
|
506
|
+
onClick={() => setShowAddRedirect(true)}
|
|
507
|
+
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 shrink-0"
|
|
508
|
+
>
|
|
509
|
+
<Plus className="w-4 h-4" />
|
|
510
|
+
Add Redirect
|
|
287
511
|
</button>
|
|
288
512
|
</div>
|
|
289
513
|
|
|
@@ -292,23 +516,63 @@ export function SEO({ onNavigate, initialTab = 'pages' }: SEOProps) {
|
|
|
292
516
|
<table className="w-full">
|
|
293
517
|
<thead className="bg-gray-50 border-b border-gray-200 sticky top-0">
|
|
294
518
|
<tr>
|
|
295
|
-
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
296
|
-
|
|
519
|
+
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
520
|
+
Source
|
|
521
|
+
</th>
|
|
522
|
+
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
523
|
+
Destination
|
|
524
|
+
</th>
|
|
297
525
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">Type</th>
|
|
298
526
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">Hits</th>
|
|
299
|
-
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
300
|
-
|
|
527
|
+
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
528
|
+
Status
|
|
529
|
+
</th>
|
|
530
|
+
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
531
|
+
Actions
|
|
532
|
+
</th>
|
|
301
533
|
</tr>
|
|
302
534
|
</thead>
|
|
303
535
|
<tbody className="divide-y divide-gray-200">
|
|
304
536
|
{filteredRedirects.map((r: any) => (
|
|
305
537
|
<tr key={r.id} className="hover:bg-gray-50 transition-colors">
|
|
306
|
-
<td className="px-4 py-3"
|
|
307
|
-
|
|
308
|
-
|
|
538
|
+
<td className="px-4 py-3">
|
|
539
|
+
<code className="rounded bg-gray-100 px-2 py-1 text-xs text-gray-900">
|
|
540
|
+
{r.from}
|
|
541
|
+
</code>
|
|
542
|
+
</td>
|
|
543
|
+
<td className="px-4 py-3">
|
|
544
|
+
<code className="rounded bg-gray-100 px-2 py-1 text-xs text-gray-900">
|
|
545
|
+
{r.to}
|
|
546
|
+
</code>
|
|
547
|
+
</td>
|
|
548
|
+
<td className="px-4 py-3">
|
|
549
|
+
<span
|
|
550
|
+
className={`px-2 py-0.5 rounded-full text-xs font-medium ${r.type === '301' ? 'bg-blue-100 text-blue-800' : 'bg-purple-100 text-purple-800'}`}
|
|
551
|
+
>
|
|
552
|
+
{r.type}
|
|
553
|
+
</span>
|
|
554
|
+
</td>
|
|
309
555
|
<td className="px-4 py-3 text-sm text-gray-600">{r.hits.toLocaleString()}</td>
|
|
310
|
-
<td className="px-4 py-3"
|
|
311
|
-
|
|
556
|
+
<td className="px-4 py-3">
|
|
557
|
+
<span
|
|
558
|
+
className={`px-2 py-0.5 rounded-full text-xs font-medium ${r.active ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}
|
|
559
|
+
>
|
|
560
|
+
{r.active ? 'Active' : 'Inactive'}
|
|
561
|
+
</span>
|
|
562
|
+
</td>
|
|
563
|
+
<td className="px-4 py-3">
|
|
564
|
+
<div className="flex items-center gap-2">
|
|
565
|
+
<button className="p-1.5 hover:bg-gray-100 rounded transition-colors">
|
|
566
|
+
<Pencil className="w-4 h-4 text-gray-600" />
|
|
567
|
+
</button>
|
|
568
|
+
<button
|
|
569
|
+
onClick={() => handleDeleteRedirect(r.id)}
|
|
570
|
+
className="p-1.5 hover:bg-gray-100 rounded transition-colors"
|
|
571
|
+
>
|
|
572
|
+
<Trash2 className="w-4 h-4 text-red-600" />
|
|
573
|
+
</button>
|
|
574
|
+
</div>
|
|
575
|
+
</td>
|
|
312
576
|
</tr>
|
|
313
577
|
))}
|
|
314
578
|
</tbody>
|
|
@@ -320,14 +584,66 @@ export function SEO({ onNavigate, initialTab = 'pages' }: SEOProps) {
|
|
|
320
584
|
<Dialog.Portal>
|
|
321
585
|
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/50" />
|
|
322
586
|
<Dialog.Content className="fixed left-1/2 top-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white p-6 shadow-lg">
|
|
323
|
-
<Dialog.Title className="mb-4 text-lg font-semibold text-gray-900">
|
|
587
|
+
<Dialog.Title className="mb-4 text-lg font-semibold text-gray-900">
|
|
588
|
+
Add Redirect
|
|
589
|
+
</Dialog.Title>
|
|
324
590
|
<form onSubmit={handleAddRedirect} className="space-y-4">
|
|
325
|
-
<div
|
|
326
|
-
|
|
327
|
-
|
|
591
|
+
<div>
|
|
592
|
+
<label className="mb-1 block text-sm font-medium text-gray-700">
|
|
593
|
+
Source URL
|
|
594
|
+
</label>
|
|
595
|
+
<input
|
|
596
|
+
type="text"
|
|
597
|
+
value={newRedirect.source}
|
|
598
|
+
onChange={(e) => setNewRedirect({ ...newRedirect, source: e.target.value })}
|
|
599
|
+
placeholder="/old-page"
|
|
600
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
601
|
+
required
|
|
602
|
+
/>
|
|
603
|
+
</div>
|
|
604
|
+
<div>
|
|
605
|
+
<label className="mb-1 block text-sm font-medium text-gray-700">
|
|
606
|
+
Destination URL
|
|
607
|
+
</label>
|
|
608
|
+
<input
|
|
609
|
+
type="text"
|
|
610
|
+
value={newRedirect.destination}
|
|
611
|
+
onChange={(e) =>
|
|
612
|
+
setNewRedirect({ ...newRedirect, destination: e.target.value })
|
|
613
|
+
}
|
|
614
|
+
placeholder="/new-page"
|
|
615
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
616
|
+
required
|
|
617
|
+
/>
|
|
618
|
+
</div>
|
|
619
|
+
<div>
|
|
620
|
+
<label className="mb-1 block text-sm font-medium text-gray-700">Type</label>
|
|
621
|
+
<select
|
|
622
|
+
value={newRedirect.type}
|
|
623
|
+
onChange={(e) =>
|
|
624
|
+
setNewRedirect({ ...newRedirect, type: e.target.value as '301' | '302' })
|
|
625
|
+
}
|
|
626
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
627
|
+
>
|
|
628
|
+
<option value="301">301 (Permanent)</option>
|
|
629
|
+
<option value="302">302 (Temporary)</option>
|
|
630
|
+
</select>
|
|
631
|
+
</div>
|
|
328
632
|
<div className="flex justify-end gap-3 pt-4">
|
|
329
|
-
<Dialog.Close asChild
|
|
330
|
-
|
|
633
|
+
<Dialog.Close asChild>
|
|
634
|
+
<button
|
|
635
|
+
type="button"
|
|
636
|
+
className="rounded-lg border border-gray-300 px-4 py-2 text-sm hover:bg-gray-50"
|
|
637
|
+
>
|
|
638
|
+
Cancel
|
|
639
|
+
</button>
|
|
640
|
+
</Dialog.Close>
|
|
641
|
+
<button
|
|
642
|
+
type="submit"
|
|
643
|
+
className="rounded-lg bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700"
|
|
644
|
+
>
|
|
645
|
+
Add Redirect
|
|
646
|
+
</button>
|
|
331
647
|
</div>
|
|
332
648
|
</form>
|
|
333
649
|
</Dialog.Content>
|
|
@@ -339,19 +655,31 @@ export function SEO({ onNavigate, initialTab = 'pages' }: SEOProps) {
|
|
|
339
655
|
<Tabs.Content value="canonicals" className="flex flex-col flex-1 min-h-0 space-y-4">
|
|
340
656
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
|
341
657
|
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
|
342
|
-
<div className="flex items-center gap-2 mb-2"
|
|
658
|
+
<div className="flex items-center gap-2 mb-2">
|
|
659
|
+
<ShieldCheck className="w-4 h-4 text-green-600" />
|
|
660
|
+
<span className="text-xs font-medium text-gray-600">With Canonical</span>
|
|
661
|
+
</div>
|
|
343
662
|
<div className="text-2xl font-semibold text-green-700">{allCanonicals.length}</div>
|
|
344
663
|
</div>
|
|
345
664
|
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
|
346
|
-
<div className="flex items-center gap-2 mb-2"
|
|
665
|
+
<div className="flex items-center gap-2 mb-2">
|
|
666
|
+
<AlertTriangle className="w-4 h-4 text-red-600" />
|
|
667
|
+
<span className="text-xs font-medium text-gray-600">Missing Canonical</span>
|
|
668
|
+
</div>
|
|
347
669
|
<div className="text-2xl font-semibold text-red-700">{missingCanonical.length}</div>
|
|
348
670
|
</div>
|
|
349
671
|
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
|
350
|
-
<div className="flex items-center gap-2 mb-2"
|
|
672
|
+
<div className="flex items-center gap-2 mb-2">
|
|
673
|
+
<Globe className="w-4 h-4 text-blue-600" />
|
|
674
|
+
<span className="text-xs font-medium text-gray-600">Canonical Domains</span>
|
|
675
|
+
</div>
|
|
351
676
|
<div className="text-2xl font-semibold text-gray-900">{canonicalDomains.length}</div>
|
|
352
677
|
</div>
|
|
353
678
|
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
|
354
|
-
<div className="flex items-center gap-2 mb-2"
|
|
679
|
+
<div className="flex items-center gap-2 mb-2">
|
|
680
|
+
<CheckCircle2 className="w-4 h-4 text-green-600" />
|
|
681
|
+
<span className="text-xs font-medium text-gray-600">Self-Referencing</span>
|
|
682
|
+
</div>
|
|
355
683
|
<div className="text-2xl font-semibold text-green-700">{allCanonicals.length}</div>
|
|
356
684
|
<div className="text-xs text-gray-500 mt-1">Correct pattern</div>
|
|
357
685
|
</div>
|
|
@@ -362,11 +690,23 @@ export function SEO({ onNavigate, initialTab = 'pages' }: SEOProps) {
|
|
|
362
690
|
<div className="flex items-start gap-3">
|
|
363
691
|
<AlertTriangle className="w-5 h-5 text-yellow-600 mt-0.5 shrink-0" />
|
|
364
692
|
<div className="flex-1">
|
|
365
|
-
<h3 className="text-sm font-semibold text-yellow-900 mb-1">
|
|
366
|
-
|
|
693
|
+
<h3 className="text-sm font-semibold text-yellow-900 mb-1">
|
|
694
|
+
{missingCanonical.length} page{missingCanonical.length !== 1 ? 's' : ''} missing
|
|
695
|
+
canonical URLs
|
|
696
|
+
</h3>
|
|
697
|
+
<p className="text-sm text-yellow-700 mb-2">
|
|
698
|
+
Without canonical tags, search engines may index duplicate versions of these
|
|
699
|
+
pages, diluting your ranking signals.
|
|
700
|
+
</p>
|
|
367
701
|
<div className="flex flex-wrap gap-2">
|
|
368
702
|
{missingCanonical.map((p: any) => (
|
|
369
|
-
<button
|
|
703
|
+
<button
|
|
704
|
+
key={p.id}
|
|
705
|
+
onClick={() => onNavigate?.(`/pages/${p.id}`)}
|
|
706
|
+
className="px-2.5 py-1 text-xs bg-yellow-100 text-yellow-900 rounded-lg hover:bg-yellow-200 transition-colors"
|
|
707
|
+
>
|
|
708
|
+
{p.title} ({p.url})
|
|
709
|
+
</button>
|
|
370
710
|
))}
|
|
371
711
|
</div>
|
|
372
712
|
</div>
|
|
@@ -381,19 +721,55 @@ export function SEO({ onNavigate, initialTab = 'pages' }: SEOProps) {
|
|
|
381
721
|
<tr>
|
|
382
722
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">Page</th>
|
|
383
723
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">URL</th>
|
|
384
|
-
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
385
|
-
|
|
386
|
-
|
|
724
|
+
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
725
|
+
Canonical URL
|
|
726
|
+
</th>
|
|
727
|
+
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
728
|
+
Status
|
|
729
|
+
</th>
|
|
730
|
+
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
731
|
+
Actions
|
|
732
|
+
</th>
|
|
387
733
|
</tr>
|
|
388
734
|
</thead>
|
|
389
735
|
<tbody className="divide-y divide-gray-200">
|
|
390
736
|
{seoPages.map((page: any) => (
|
|
391
737
|
<tr key={page.id} className="hover:bg-gray-50 transition-colors">
|
|
392
738
|
<td className="px-4 py-3 text-sm font-medium text-gray-900">{page.title}</td>
|
|
393
|
-
<td className="px-4 py-3"
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
739
|
+
<td className="px-4 py-3">
|
|
740
|
+
<code className="text-xs bg-gray-100 px-2 py-1 rounded text-gray-700">
|
|
741
|
+
{page.url}
|
|
742
|
+
</code>
|
|
743
|
+
</td>
|
|
744
|
+
<td className="px-4 py-3">
|
|
745
|
+
{page.canonical ? (
|
|
746
|
+
<code className="text-xs bg-gray-100 px-2 py-1 rounded text-gray-700 truncate max-w-[200px] block">
|
|
747
|
+
{page.canonical}
|
|
748
|
+
</code>
|
|
749
|
+
) : (
|
|
750
|
+
<span className="text-xs text-red-600 font-medium">Not set</span>
|
|
751
|
+
)}
|
|
752
|
+
</td>
|
|
753
|
+
<td className="px-4 py-3">
|
|
754
|
+
{page.canonical ? (
|
|
755
|
+
<span className="px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
756
|
+
✓ Set
|
|
757
|
+
</span>
|
|
758
|
+
) : (
|
|
759
|
+
<span className="px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
|
760
|
+
Missing
|
|
761
|
+
</span>
|
|
762
|
+
)}
|
|
763
|
+
</td>
|
|
764
|
+
<td className="px-4 py-3">
|
|
765
|
+
<button
|
|
766
|
+
onClick={() => onNavigate?.(`/pages/${page.id}`)}
|
|
767
|
+
className="p-1.5 hover:bg-gray-100 rounded transition-colors"
|
|
768
|
+
title="Edit"
|
|
769
|
+
>
|
|
770
|
+
<Pencil className="w-4 h-4 text-gray-600" />
|
|
771
|
+
</button>
|
|
772
|
+
</td>
|
|
397
773
|
</tr>
|
|
398
774
|
))}
|
|
399
775
|
</tbody>
|
|
@@ -407,19 +783,27 @@ export function SEO({ onNavigate, initialTab = 'pages' }: SEOProps) {
|
|
|
407
783
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
|
408
784
|
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
|
409
785
|
<div className="text-xs text-gray-600 mb-1">Broken Links</div>
|
|
410
|
-
<div className="text-2xl font-semibold text-red-600">
|
|
786
|
+
<div className="text-2xl font-semibold text-red-600">
|
|
787
|
+
{linkHealth.filter((l: any) => l.status === 404).length}
|
|
788
|
+
</div>
|
|
411
789
|
</div>
|
|
412
790
|
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
|
413
791
|
<div className="text-xs text-gray-600 mb-1">Redirect Chains</div>
|
|
414
|
-
<div className="text-2xl font-semibold text-yellow-600">
|
|
792
|
+
<div className="text-2xl font-semibold text-yellow-600">
|
|
793
|
+
{linkHealth.filter((l: any) => l.status === 301).length}
|
|
794
|
+
</div>
|
|
415
795
|
</div>
|
|
416
796
|
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
|
417
797
|
<div className="text-xs text-gray-600 mb-1">Internal Issues</div>
|
|
418
|
-
<div className="text-2xl font-semibold text-gray-900">
|
|
798
|
+
<div className="text-2xl font-semibold text-gray-900">
|
|
799
|
+
{linkHealth.filter((l: any) => l.type === 'internal').length}
|
|
800
|
+
</div>
|
|
419
801
|
</div>
|
|
420
802
|
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
|
421
803
|
<div className="text-xs text-gray-600 mb-1">External Issues</div>
|
|
422
|
-
<div className="text-2xl font-semibold text-gray-900">
|
|
804
|
+
<div className="text-2xl font-semibold text-gray-900">
|
|
805
|
+
{linkHealth.filter((l: any) => l.type === 'external').length}
|
|
806
|
+
</div>
|
|
423
807
|
</div>
|
|
424
808
|
</div>
|
|
425
809
|
|
|
@@ -428,23 +812,68 @@ export function SEO({ onNavigate, initialTab = 'pages' }: SEOProps) {
|
|
|
428
812
|
<table className="w-full">
|
|
429
813
|
<thead className="bg-gray-50 border-b border-gray-200 sticky top-0">
|
|
430
814
|
<tr>
|
|
431
|
-
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
432
|
-
|
|
433
|
-
|
|
815
|
+
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
816
|
+
Found On
|
|
817
|
+
</th>
|
|
818
|
+
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
819
|
+
Broken URL
|
|
820
|
+
</th>
|
|
821
|
+
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
822
|
+
Status
|
|
823
|
+
</th>
|
|
434
824
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">Type</th>
|
|
435
|
-
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
436
|
-
|
|
825
|
+
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
826
|
+
Last Checked
|
|
827
|
+
</th>
|
|
828
|
+
<th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
|
829
|
+
Actions
|
|
830
|
+
</th>
|
|
437
831
|
</tr>
|
|
438
832
|
</thead>
|
|
439
833
|
<tbody className="divide-y divide-gray-200">
|
|
440
834
|
{linkHealth.map((link: any) => (
|
|
441
835
|
<tr key={link.id} className="hover:bg-gray-50 transition-colors">
|
|
442
|
-
<td className="px-4 py-3"
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
836
|
+
<td className="px-4 py-3">
|
|
837
|
+
<code className="text-xs bg-gray-100 px-2 py-1 rounded text-gray-700">
|
|
838
|
+
{link.page}
|
|
839
|
+
</code>
|
|
840
|
+
</td>
|
|
841
|
+
<td className="px-4 py-3">
|
|
842
|
+
<code className="text-xs bg-red-50 px-2 py-1 rounded text-red-800 truncate max-w-[200px] block">
|
|
843
|
+
{link.url}
|
|
844
|
+
</code>
|
|
845
|
+
</td>
|
|
846
|
+
<td className="px-4 py-3">
|
|
847
|
+
<span
|
|
848
|
+
className={`px-2 py-0.5 rounded-full text-xs font-medium ${link.status === 404 ? 'bg-red-100 text-red-800' : 'bg-yellow-100 text-yellow-800'}`}
|
|
849
|
+
>
|
|
850
|
+
{link.status}
|
|
851
|
+
</span>
|
|
852
|
+
</td>
|
|
853
|
+
<td className="px-4 py-3">
|
|
854
|
+
<span
|
|
855
|
+
className={`px-2 py-0.5 rounded-full text-xs font-medium ${link.type === 'internal' ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800'}`}
|
|
856
|
+
>
|
|
857
|
+
{link.type}
|
|
858
|
+
</span>
|
|
859
|
+
</td>
|
|
446
860
|
<td className="px-4 py-3 text-sm text-gray-600">{link.lastChecked}</td>
|
|
447
|
-
<td className="px-4 py-3"
|
|
861
|
+
<td className="px-4 py-3">
|
|
862
|
+
<div className="flex items-center gap-1">
|
|
863
|
+
<button
|
|
864
|
+
className="p-1.5 hover:bg-gray-100 rounded transition-colors"
|
|
865
|
+
title="Create redirect"
|
|
866
|
+
>
|
|
867
|
+
<ArrowRightLeft className="w-4 h-4 text-gray-600" />
|
|
868
|
+
</button>
|
|
869
|
+
<button
|
|
870
|
+
className="p-1.5 hover:bg-gray-100 rounded transition-colors"
|
|
871
|
+
title="Open URL"
|
|
872
|
+
>
|
|
873
|
+
<ExternalLink className="w-4 h-4 text-gray-600" />
|
|
874
|
+
</button>
|
|
875
|
+
</div>
|
|
876
|
+
</td>
|
|
448
877
|
</tr>
|
|
449
878
|
))}
|
|
450
879
|
</tbody>
|
|
@@ -454,5 +883,5 @@ export function SEO({ onNavigate, initialTab = 'pages' }: SEOProps) {
|
|
|
454
883
|
</Tabs.Content>
|
|
455
884
|
</Tabs.Root>
|
|
456
885
|
</div>
|
|
457
|
-
)
|
|
886
|
+
)
|
|
458
887
|
}
|