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