@actuate-media/cms-admin 0.9.0 → 0.11.0
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 +8 -5
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/layout/primitives.test.d.ts +2 -0
- package/dist/__tests__/layout/primitives.test.d.ts.map +1 -0
- package/dist/__tests__/layout/primitives.test.js +34 -0
- package/dist/__tests__/layout/primitives.test.js.map +1 -0
- package/dist/__tests__/lib/cv.test.d.ts +2 -0
- package/dist/__tests__/lib/cv.test.d.ts.map +1 -0
- package/dist/__tests__/lib/cv.test.js +66 -0
- package/dist/__tests__/lib/cv.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/assets/actuate-logo.d.ts +36 -0
- package/dist/assets/actuate-logo.d.ts.map +1 -0
- package/dist/assets/actuate-logo.js +15 -0
- package/dist/assets/actuate-logo.js.map +1 -0
- package/dist/components/Breadcrumbs.js +2 -2
- package/dist/components/CommandPalette.js +10 -10
- package/dist/components/ContentOverviewChart.js +3 -3
- package/dist/components/ErrorBoundary.js +1 -1
- package/dist/components/FocalPointPicker.js +2 -2
- package/dist/components/FolderTree.js +20 -20
- package/dist/components/LivePreview.js +3 -3
- package/dist/components/LocaleSwitcher.js +1 -1
- package/dist/components/MediaPickerModal.js +4 -4
- package/dist/components/PresenceIndicator.js +1 -1
- package/dist/components/SEOConfigPanel.d.ts +2 -0
- package/dist/components/SEOConfigPanel.d.ts.map +1 -0
- package/dist/components/SEOConfigPanel.js +174 -0
- package/dist/components/SEOConfigPanel.js.map +1 -0
- package/dist/components/SEOPanel.js +9 -9
- package/dist/components/SEOPerformance.js +2 -2
- package/dist/components/SchedulePublishDialog.d.ts +18 -0
- package/dist/components/SchedulePublishDialog.d.ts.map +1 -0
- package/dist/components/SchedulePublishDialog.js +106 -0
- package/dist/components/SchedulePublishDialog.js.map +1 -0
- package/dist/components/SharePreviewLinkDialog.d.ts +17 -0
- package/dist/components/SharePreviewLinkDialog.d.ts.map +1 -0
- package/dist/components/SharePreviewLinkDialog.js +83 -0
- package/dist/components/SharePreviewLinkDialog.js.map +1 -0
- package/dist/components/TipTapEditor.js +5 -5
- package/dist/components/VersionHistory.js +2 -2
- package/dist/components/ui/Badge.d.ts +33 -3
- package/dist/components/ui/Badge.d.ts.map +1 -1
- package/dist/components/ui/Badge.js +42 -8
- package/dist/components/ui/Badge.js.map +1 -1
- package/dist/components/ui/Button.d.ts +19 -8
- package/dist/components/ui/Button.d.ts.map +1 -1
- package/dist/components/ui/Button.js +35 -14
- package/dist/components/ui/Button.js.map +1 -1
- package/dist/components/ui/Card.d.ts +26 -0
- package/dist/components/ui/Card.d.ts.map +1 -0
- package/dist/components/ui/Card.js +45 -0
- package/dist/components/ui/Card.js.map +1 -0
- package/dist/components/ui/DataTable.js +1 -1
- package/dist/components/ui/Input.d.ts +15 -0
- package/dist/components/ui/Input.d.ts.map +1 -0
- package/dist/components/ui/Input.js +23 -0
- package/dist/components/ui/Input.js.map +1 -0
- package/dist/components/ui/SearchInput.js +1 -1
- package/dist/components/ui/Select.d.ts +16 -0
- package/dist/components/ui/Select.d.ts.map +1 -0
- package/dist/components/ui/Select.js +25 -0
- package/dist/components/ui/Select.js.map +1 -0
- package/dist/components/ui/Toast.js +1 -1
- package/dist/components/ui/index.d.ts +10 -4
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +5 -2
- package/dist/components/ui/index.js.map +1 -1
- package/dist/fields/BlockBuilderField.js +3 -3
- package/dist/fields/DateField.js +1 -1
- package/dist/fields/RelationshipField.js +3 -3
- package/dist/fields/TextField.js +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/layout/Header.js +1 -1
- package/dist/layout/Layout.d.ts +14 -0
- package/dist/layout/Layout.d.ts.map +1 -1
- package/dist/layout/Layout.js +17 -11
- package/dist/layout/Layout.js.map +1 -1
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +21 -11
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/layout/primitives/AdminShell.d.ts +43 -0
- package/dist/layout/primitives/AdminShell.d.ts.map +1 -0
- package/dist/layout/primitives/AdminShell.js +51 -0
- package/dist/layout/primitives/AdminShell.js.map +1 -0
- package/dist/layout/primitives/Box.d.ts +19 -0
- package/dist/layout/primitives/Box.d.ts.map +1 -0
- package/dist/layout/primitives/Box.js +12 -0
- package/dist/layout/primitives/Box.js.map +1 -0
- package/dist/layout/primitives/Cluster.d.ts +27 -0
- package/dist/layout/primitives/Cluster.d.ts.map +1 -0
- package/dist/layout/primitives/Cluster.js +37 -0
- package/dist/layout/primitives/Cluster.js.map +1 -0
- package/dist/layout/primitives/Grid.d.ts +45 -0
- package/dist/layout/primitives/Grid.d.ts.map +1 -0
- package/dist/layout/primitives/Grid.js +59 -0
- package/dist/layout/primitives/Grid.js.map +1 -0
- package/dist/layout/primitives/PageContainer.d.ts +36 -0
- package/dist/layout/primitives/PageContainer.d.ts.map +1 -0
- package/dist/layout/primitives/PageContainer.js +41 -0
- package/dist/layout/primitives/PageContainer.js.map +1 -0
- package/dist/layout/primitives/Split.d.ts +34 -0
- package/dist/layout/primitives/Split.d.ts.map +1 -0
- package/dist/layout/primitives/Split.js +27 -0
- package/dist/layout/primitives/Split.js.map +1 -0
- package/dist/layout/primitives/Stack.d.ts +23 -0
- package/dist/layout/primitives/Stack.d.ts.map +1 -0
- package/dist/layout/primitives/Stack.js +34 -0
- package/dist/layout/primitives/Stack.js.map +1 -0
- package/dist/layout/primitives/index.d.ts +30 -0
- package/dist/layout/primitives/index.d.ts.map +1 -0
- package/dist/layout/primitives/index.js +22 -0
- package/dist/layout/primitives/index.js.map +1 -0
- package/dist/layout/primitives/tokens.d.ts +48 -0
- package/dist/layout/primitives/tokens.d.ts.map +1 -0
- package/dist/layout/primitives/tokens.js +54 -0
- package/dist/layout/primitives/tokens.js.map +1 -0
- package/dist/lib/cv.d.ts +53 -0
- package/dist/lib/cv.d.ts.map +1 -0
- package/dist/lib/cv.js +39 -0
- package/dist/lib/cv.js.map +1 -0
- package/dist/views/ApiKeys.d.ts.map +1 -1
- package/dist/views/ApiKeys.js +13 -11
- package/dist/views/ApiKeys.js.map +1 -1
- package/dist/views/CollectionList.js +8 -8
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +333 -78
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/DocumentEdit.d.ts.map +1 -1
- package/dist/views/DocumentEdit.js +17 -5
- package/dist/views/DocumentEdit.js.map +1 -1
- package/dist/views/ForgotPassword.js +2 -2
- package/dist/views/FormEditor.js +5 -5
- package/dist/views/FormSubmissions.js +6 -6
- package/dist/views/Forms.js +2 -2
- package/dist/views/Login.d.ts +16 -1
- package/dist/views/Login.d.ts.map +1 -1
- package/dist/views/Login.js +17 -7
- package/dist/views/Login.js.map +1 -1
- package/dist/views/MediaBrowser.js +16 -16
- package/dist/views/PageEditor.js +2 -2
- package/dist/views/Pages.js +10 -10
- package/dist/views/PostEditor.js +2 -2
- package/dist/views/Posts.js +4 -4
- package/dist/views/Redirects.js +4 -4
- package/dist/views/ResetPassword.js +2 -2
- package/dist/views/SEO.js +6 -6
- package/dist/views/ScriptTagEditor.js +4 -4
- package/dist/views/ScriptTags.js +2 -2
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +9 -8
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/SetupWizard.js +2 -2
- package/dist/views/Users.js +4 -4
- package/dist/views/page-builder/AIBlockAssist.js +1 -1
- package/dist/views/page-builder/AIGenerateDialog.js +10 -10
- package/dist/views/page-builder/BlockEditor.js +10 -10
- package/dist/views/page-builder/BlockPicker.js +4 -4
- package/dist/views/page-builder/BottomBar.js +1 -1
- package/dist/views/page-builder/BuilderToolbar.js +2 -2
- package/dist/views/page-builder/ContextPanel.js +2 -2
- package/dist/views/page-builder/DesignScore.js +9 -9
- package/dist/views/page-builder/NodeSettings.js +8 -8
- package/dist/views/page-builder/PageBuilder.js +3 -3
- package/dist/views/page-builder/PageSettings.js +1 -1
- package/dist/views/page-builder/PageTemplates.js +2 -2
- package/dist/views/page-builder/SEOPanel.js +13 -13
- package/dist/views/page-builder/SavedSections.js +5 -5
- package/dist/views/page-builder/TemplatePicker.js +2 -2
- package/dist/views/page-builder/block-renderers/CTAPreview.js +5 -5
- package/dist/views/page-builder/block-renderers/CardsPreview.js +1 -1
- package/dist/views/page-builder/block-renderers/CodePreview.js +1 -1
- package/dist/views/page-builder/block-renderers/FAQPreview.js +3 -3
- package/dist/views/page-builder/block-renderers/FallbackPreview.js +1 -1
- package/dist/views/page-builder/block-renderers/FormPreview.js +3 -3
- package/dist/views/page-builder/block-renderers/GalleryPreview.js +5 -5
- package/dist/views/page-builder/block-renderers/HeroPreview.js +3 -3
- package/dist/views/page-builder/block-renderers/ImagePreview.js +3 -3
- package/dist/views/page-builder/block-renderers/TextPreview.js +3 -3
- package/dist/views/page-builder/block-renderers/VideoPreview.js +4 -4
- package/dist/views/page-builder/canvas/BlockRenderer.js +1 -1
- package/dist/views/page-builder/canvas/BuilderCanvas.js +3 -3
- package/dist/views/page-builder/canvas/ColumnRenderer.js +2 -2
- package/dist/views/page-builder/canvas/ContainerRenderer.js +2 -2
- package/dist/views/page-builder/canvas/RowRenderer.js +2 -2
- package/dist/views/page-builder/canvas/SectionRenderer.js +2 -2
- package/package.json +6 -2
- package/src/AdminRoot.tsx +21 -11
- package/src/__tests__/layout/primitives.test.ts +37 -0
- package/src/__tests__/lib/cv.test.ts +74 -0
- package/src/assets/actuate-logo.tsx +72 -0
- package/src/components/Breadcrumbs.tsx +6 -6
- package/src/components/CommandPalette.tsx +34 -34
- package/src/components/ContentOverviewChart.tsx +3 -3
- package/src/components/ErrorBoundary.tsx +3 -3
- package/src/components/FocalPointPicker.tsx +4 -4
- package/src/components/FolderTree.tsx +38 -38
- package/src/components/LivePreview.tsx +16 -16
- package/src/components/LocaleSwitcher.tsx +7 -7
- package/src/components/MediaPickerModal.tsx +21 -21
- package/src/components/PresenceIndicator.tsx +2 -2
- package/src/components/SEOConfigPanel.tsx +582 -0
- package/src/components/SEOPanel.tsx +46 -46
- package/src/components/SEOPerformance.tsx +21 -21
- package/src/components/SchedulePublishDialog.tsx +241 -0
- package/src/components/SharePreviewLinkDialog.tsx +227 -0
- package/src/components/TipTapEditor.tsx +33 -33
- package/src/components/VersionHistory.tsx +16 -16
- package/src/components/ui/Badge.tsx +66 -14
- package/src/components/ui/Button.tsx +70 -33
- package/src/components/ui/Card.tsx +101 -0
- package/src/components/ui/DataTable.tsx +1 -1
- package/src/components/ui/Input.tsx +35 -0
- package/src/components/ui/SearchInput.tsx +4 -4
- package/src/components/ui/Select.tsx +56 -0
- package/src/components/ui/Toast.tsx +1 -1
- package/src/components/ui/index.ts +18 -4
- package/src/fields/BlockBuilderField.tsx +3 -3
- package/src/fields/DateField.tsx +1 -1
- package/src/fields/RelationshipField.tsx +10 -10
- package/src/fields/TextField.tsx +1 -1
- package/src/index.ts +32 -0
- package/src/layout/Header.tsx +28 -28
- package/src/layout/Layout.tsx +39 -46
- package/src/layout/Sidebar.tsx +37 -64
- package/src/layout/primitives/AdminShell.tsx +118 -0
- package/src/layout/primitives/Box.tsx +30 -0
- package/src/layout/primitives/Cluster.tsx +74 -0
- package/src/layout/primitives/Grid.tsx +120 -0
- package/src/layout/primitives/PageContainer.tsx +96 -0
- package/src/layout/primitives/Split.tsx +73 -0
- package/src/layout/primitives/Stack.tsx +67 -0
- package/src/layout/primitives/index.ts +36 -0
- package/src/layout/primitives/tokens.ts +76 -0
- package/src/lib/cv.ts +96 -0
- package/src/styles/build-input.css +1 -1
- package/src/views/ApiKeys.tsx +57 -57
- package/src/views/CollectionList.tsx +30 -30
- package/src/views/Dashboard.tsx +737 -186
- package/src/views/DocumentEdit.tsx +90 -10
- package/src/views/ForgotPassword.tsx +18 -18
- package/src/views/FormEditor.tsx +75 -75
- package/src/views/FormSubmissions.tsx +76 -76
- package/src/views/Forms.tsx +27 -27
- package/src/views/Login.tsx +65 -25
- package/src/views/MediaBrowser.tsx +127 -127
- package/src/views/PageEditor.tsx +25 -25
- package/src/views/Pages.tsx +59 -59
- package/src/views/PostEditor.tsx +37 -37
- package/src/views/Posts.tsx +48 -48
- package/src/views/Redirects.tsx +21 -21
- package/src/views/ResetPassword.tsx +28 -28
- package/src/views/SEO.tsx +144 -144
- package/src/views/ScriptTagEditor.tsx +24 -24
- package/src/views/ScriptTags.tsx +10 -10
- package/src/views/Settings.tsx +88 -80
- package/src/views/SetupWizard.tsx +28 -28
- package/src/views/Users.tsx +20 -20
- package/src/views/page-builder/AIBlockAssist.tsx +1 -1
- package/src/views/page-builder/AIGenerateDialog.tsx +63 -63
- package/src/views/page-builder/BlockEditor.tsx +26 -26
- package/src/views/page-builder/BlockPicker.tsx +22 -22
- package/src/views/page-builder/BottomBar.tsx +8 -8
- package/src/views/page-builder/BuilderToolbar.tsx +17 -17
- package/src/views/page-builder/ContextPanel.tsx +3 -3
- package/src/views/page-builder/DesignScore.tsx +21 -21
- package/src/views/page-builder/NodeSettings.tsx +27 -27
- package/src/views/page-builder/PageBuilder.tsx +11 -11
- package/src/views/page-builder/PageSettings.tsx +4 -4
- package/src/views/page-builder/PageTemplates.tsx +18 -18
- package/src/views/page-builder/SEOPanel.tsx +53 -53
- package/src/views/page-builder/SavedSections.tsx +37 -37
- package/src/views/page-builder/TemplatePicker.tsx +17 -17
- package/src/views/page-builder/block-renderers/CTAPreview.tsx +13 -13
- package/src/views/page-builder/block-renderers/CardsPreview.tsx +5 -5
- package/src/views/page-builder/block-renderers/CodePreview.tsx +6 -6
- package/src/views/page-builder/block-renderers/FAQPreview.tsx +13 -13
- package/src/views/page-builder/block-renderers/FallbackPreview.tsx +3 -3
- package/src/views/page-builder/block-renderers/FormPreview.tsx +20 -20
- package/src/views/page-builder/block-renderers/GalleryPreview.tsx +8 -8
- package/src/views/page-builder/block-renderers/HeroPreview.tsx +16 -16
- package/src/views/page-builder/block-renderers/ImagePreview.tsx +4 -4
- package/src/views/page-builder/block-renderers/TextPreview.tsx +14 -14
- package/src/views/page-builder/block-renderers/VideoPreview.tsx +12 -12
- package/src/views/page-builder/canvas/BlockRenderer.tsx +4 -4
- package/src/views/page-builder/canvas/BuilderCanvas.tsx +6 -6
- package/src/views/page-builder/canvas/ColumnRenderer.tsx +3 -3
- package/src/views/page-builder/canvas/ContainerRenderer.tsx +2 -2
- package/src/views/page-builder/canvas/RowRenderer.tsx +2 -2
- package/src/views/page-builder/canvas/SectionRenderer.tsx +2 -2
|
@@ -120,27 +120,27 @@ export function CollectionList({ collectionSlug, config, onNavigate }) {
|
|
|
120
120
|
setSelected(new Set());
|
|
121
121
|
refetch();
|
|
122
122
|
};
|
|
123
|
-
const SortCol = ({ field, children }) => (_jsxs("button", { type: "button", onClick: () => toggleSort(field), className: "flex items-center gap-1 text-xs font-medium hover:opacity-80
|
|
123
|
+
const SortCol = ({ field, children }) => (_jsxs("button", { type: "button", onClick: () => toggleSort(field), className: "flex items-center gap-1 text-xs font-medium transition-opacity hover:opacity-80", style: { color: 'var(--actuate-text-secondary, #6b7280)' }, children: [children, sort === field && _jsx("span", { children: order === 'asc' ? '↑' : '↓' })] }));
|
|
124
124
|
if (loading && docs.length === 0) {
|
|
125
|
-
return (_jsx("div", { className: "
|
|
125
|
+
return (_jsx("div", { className: "flex h-64 items-center justify-center p-4", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin", style: { color: 'var(--actuate-primary, #2563eb)' } }) }));
|
|
126
126
|
}
|
|
127
|
-
return (_jsxs("div", { className: "p-3 pr-6 sm:p-4 sm:pr-8
|
|
127
|
+
return (_jsxs("div", { className: "flex h-full flex-col p-3 pr-6 sm:p-4 sm:pr-8", children: [_jsxs("div", { className: "mb-4 flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-xl font-semibold sm:text-2xl", style: { color: 'var(--actuate-text, #111827)' }, children: labels.plural }), _jsxs("p", { className: "text-sm", style: { color: 'var(--actuate-text-secondary, #6b7280)' }, children: [total, " total"] })] }), _jsxs("button", { onClick: () => onNavigate(`/${collectionSlug}/new`), className: "flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white transition-colors", style: { background: 'var(--actuate-primary, #2563eb)' }, children: [_jsx(Plus, { className: "h-4 w-4" }), " New ", labels.singular] })] }), _jsxs("div", { className: "relative mb-4", children: [_jsx(Search, { className: "absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2", style: { color: 'var(--actuate-text-muted, #9ca3af)' } }), _jsx("input", { type: "text", placeholder: `Search ${labels.plural.toLowerCase()}...`, value: search, onChange: (e) => setSearch(e.target.value), className: "w-full rounded-lg border py-2 pr-3 pl-9 text-sm focus:ring-2 focus:outline-none", style: {
|
|
128
128
|
borderColor: 'var(--actuate-border, #d1d5db)',
|
|
129
129
|
color: 'var(--actuate-text, #111827)',
|
|
130
|
-
} })] }), selected.size > 0 && (_jsxs("div", { className: "
|
|
130
|
+
} })] }), selected.size > 0 && (_jsxs("div", { className: "mb-4 flex flex-wrap items-center justify-between gap-2 rounded-lg p-3", style: {
|
|
131
131
|
background: 'var(--actuate-info-bg, #eff6ff)',
|
|
132
132
|
borderColor: 'var(--actuate-info-border, #bfdbfe)',
|
|
133
133
|
borderWidth: 1,
|
|
134
134
|
borderStyle: 'solid',
|
|
135
|
-
}, children: [_jsxs("span", { className: "text-sm", style: { color: 'var(--actuate-info-text, #1e40af)' }, children: [selected.size, " selected"] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { onClick: () => bulkAction('publish'), className: "px-3 py-1.5 text-sm text-white
|
|
135
|
+
}, children: [_jsxs("span", { className: "text-sm", style: { color: 'var(--actuate-info-text, #1e40af)' }, children: [selected.size, " selected"] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { onClick: () => bulkAction('publish'), className: "rounded-lg px-3 py-1.5 text-sm text-white", style: { background: 'var(--actuate-success, #16a34a)' }, children: "Publish" }), _jsx("button", { onClick: () => bulkAction('unpublish'), className: "rounded-lg px-3 py-1.5 text-sm text-white", style: { background: 'var(--actuate-warning, #ca8a04)' }, children: "Unpublish" }), _jsxs("button", { onClick: () => bulkAction('delete'), className: "rounded-lg px-3 py-1.5 text-sm text-white", style: { background: 'var(--actuate-danger, #dc2626)' }, children: [_jsx(Trash2, { className: "-mt-0.5 mr-1 inline h-3.5 w-3.5" }), "Delete"] })] })] })), error && (_jsxs("div", { className: "mb-4 rounded-lg p-3 text-sm", style: {
|
|
136
136
|
background: 'var(--actuate-danger-bg, #fef2f2)',
|
|
137
137
|
color: 'var(--actuate-danger-text, #991b1b)',
|
|
138
|
-
}, children: [error, " \u2014", ' ', _jsx("button", { onClick: refetch, className: "underline", children: "retry" })] })), docs.length === 0 && !loading ? (_jsxs("div", { className: "flex-1 flex
|
|
138
|
+
}, children: [error, " \u2014", ' ', _jsx("button", { onClick: refetch, className: "underline", children: "retry" })] })), docs.length === 0 && !loading ? (_jsxs("div", { className: "flex flex-1 flex-col items-center justify-center rounded-lg border p-8", style: {
|
|
139
139
|
borderColor: 'var(--actuate-border, #d1d5db)',
|
|
140
140
|
color: 'var(--actuate-text-secondary, #6b7280)',
|
|
141
|
-
}, children: [_jsx(FileText, { className: "
|
|
141
|
+
}, children: [_jsx(FileText, { className: "mb-3 h-10 w-10 opacity-40" }), _jsxs("p", { className: "mb-3 text-sm", children: ["No ", labels.plural.toLowerCase(), " found"] }), _jsxs("button", { onClick: () => onNavigate(`/${collectionSlug}/new`), className: "rounded-lg px-4 py-2 text-sm text-white", style: { background: 'var(--actuate-primary, #2563eb)' }, children: ["Create ", labels.singular] })] })) : (_jsx("div", { className: "flex-1 overflow-auto rounded-lg border", style: { borderColor: 'var(--actuate-border, #d1d5db)' }, children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "sticky top-0", style: {
|
|
142
142
|
background: 'var(--actuate-surface-alt, #f9fafb)',
|
|
143
143
|
borderBottom: '1px solid var(--actuate-border, #d1d5db)',
|
|
144
|
-
}, children: _jsxs("tr", { children: [_jsx("th", { className: "w-10 px-3 py-2 text-left", children: _jsx("input", { type: "checkbox", checked: selected.size === docs.length && docs.length > 0, onChange: (e) => toggleAll(e.target.checked), className: "rounded" }) }), _jsx("th", { className: "px-3 py-2 text-left", children: _jsx(SortCol, { field: "title", children: "Title" }) }), _jsx("th", { className: "px-3 py-2 text-left", children: _jsx(SortCol, { field: "status", children: "Status" }) }), _jsx("th", { className: "px-3 py-2 text-left", children: _jsx(SortCol, { field: "updatedAt", children: "Updated" }) }), _jsx("th", { className: "px-3 py-2 text-left text-xs font-medium", style: { color: 'var(--actuate-text-secondary, #6b7280)' }, children: "Actions" })] }) }), _jsx("tbody", { children: docs.map((doc) => (_jsxs("tr", { className: "transition-colors hover:opacity-95", style: { borderBottom: '1px solid var(--actuate-border, #e5e7eb)' }, children: [_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", checked: selected.has(String(doc.id)), onChange: () => toggleSelect(String(doc.id)), className: "rounded" }) }), _jsx("td", { className: "px-3 py-2", children: _jsx("button", { type: "button", onClick: () => onNavigate(`/${collectionSlug}/${doc.id}`), className: "font-medium
|
|
144
|
+
}, children: _jsxs("tr", { children: [_jsx("th", { className: "w-10 px-3 py-2 text-left", children: _jsx("input", { type: "checkbox", checked: selected.size === docs.length && docs.length > 0, onChange: (e) => toggleAll(e.target.checked), className: "rounded" }) }), _jsx("th", { className: "px-3 py-2 text-left", children: _jsx(SortCol, { field: "title", children: "Title" }) }), _jsx("th", { className: "px-3 py-2 text-left", children: _jsx(SortCol, { field: "status", children: "Status" }) }), _jsx("th", { className: "px-3 py-2 text-left", children: _jsx(SortCol, { field: "updatedAt", children: "Updated" }) }), _jsx("th", { className: "px-3 py-2 text-left text-xs font-medium", style: { color: 'var(--actuate-text-secondary, #6b7280)' }, children: "Actions" })] }) }), _jsx("tbody", { children: docs.map((doc) => (_jsxs("tr", { className: "transition-colors hover:opacity-95", style: { borderBottom: '1px solid var(--actuate-border, #e5e7eb)' }, children: [_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", checked: selected.has(String(doc.id)), onChange: () => toggleSelect(String(doc.id)), className: "rounded" }) }), _jsx("td", { className: "px-3 py-2", children: _jsx("button", { type: "button", onClick: () => onNavigate(`/${collectionSlug}/${doc.id}`), className: "text-left font-medium hover:underline", style: { color: 'var(--actuate-text, #111827)' }, children: doc.title || doc.name || `#${doc.id}` }) }), _jsx("td", { className: "px-3 py-2", children: _jsx("span", { className: "inline-block rounded-full px-2 py-0.5 text-xs font-medium", style: { background: statusColor(doc.status), color: statusText(doc.status) }, children: doc.status ?? 'DRAFT' }) }), _jsx("td", { className: "px-3 py-2", style: { color: 'var(--actuate-text-secondary, #6b7280)' }, children: formatDate(doc.updatedAt) }), _jsx("td", { className: "px-3 py-2", children: _jsx("button", { type: "button", onClick: () => onNavigate(`/${collectionSlug}/${doc.id}`), className: "rounded p-1.5 transition-opacity hover:opacity-75", title: "Edit", children: _jsx(MoreHorizontal, { className: "h-4 w-4", style: { color: 'var(--actuate-text-secondary, #6b7280)' } }) }) })] }, doc.id))) })] }) })), totalPages > 1 && (_jsxs("div", { className: "mt-4 flex items-center justify-between text-sm", style: { color: 'var(--actuate-text-secondary, #6b7280)' }, children: [_jsxs("span", { children: ["Page ", page, " of ", totalPages] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs("button", { disabled: page <= 1, onClick: () => setPage((p) => p - 1), className: "flex items-center gap-1 rounded-lg border px-3 py-1.5 transition-opacity disabled:opacity-40", style: { borderColor: 'var(--actuate-border, #d1d5db)' }, children: [_jsx(ChevronLeft, { className: "h-4 w-4" }), " Previous"] }), _jsxs("button", { disabled: page >= totalPages, onClick: () => setPage((p) => p + 1), className: "flex items-center gap-1 rounded-lg border px-3 py-1.5 transition-opacity disabled:opacity-40", style: { borderColor: 'var(--actuate-border, #d1d5db)' }, children: ["Next ", _jsx(ChevronRight, { className: "h-4 w-4" })] })] })] }))] }));
|
|
145
145
|
}
|
|
146
146
|
//# sourceMappingURL=CollectionList.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Dashboard.d.ts","sourceRoot":"","sources":["../../src/views/Dashboard.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Dashboard.d.ts","sourceRoot":"","sources":["../../src/views/Dashboard.tsx"],"names":[],"mappings":"AAwEA,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,GAAG,CAAA;IACZ,OAAO,CAAC,EAAE,GAAG,CAAA;IACb,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACpC;AA0HD,wBAAgB,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,cAAc,2CAinBxE"}
|
package/dist/views/Dashboard.js
CHANGED
|
@@ -1,124 +1,379 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* Dashboard
|
|
5
|
+
* ----------
|
|
6
|
+
* Single-file, lean dashboard implementation. Two API calls only
|
|
7
|
+
* (`/stats` + `/health`, both shared with the rest of the admin), everything
|
|
8
|
+
* else derived client-side via `useMemo`. No charting libraries on this view
|
|
9
|
+
* (the design uses plain progress bars + colored dots), keeping the bundle
|
|
10
|
+
* and TTI cost low.
|
|
11
|
+
*
|
|
12
|
+
* Responsive layout:
|
|
13
|
+
* < 640px (mobile): stat cards 2-col, main grid stacks, quick actions scroll-x
|
|
14
|
+
* 640-1023 (tablet): stat cards 3-col, main grid stacks
|
|
15
|
+
* >= 1024 (desktop): stat cards 5-col, main grid is `1fr 320px`
|
|
16
|
+
*/
|
|
17
|
+
import { useMemo, useState } from 'react';
|
|
18
|
+
import { FileText, File as FileIcon, Image as ImageIcon, ClipboardList, Search, Plus, Upload, Globe, ExternalLink, Activity, AlertTriangle, Clock, Zap, Database, ChevronRight, Loader2, } from 'lucide-react';
|
|
5
19
|
import { useApiData } from '../lib/useApiData.js';
|
|
6
|
-
|
|
7
|
-
import { SEOPerformance } from '../components/SEOPerformance.js';
|
|
20
|
+
// ─── helpers (kept top-level so they aren't re-created on every render) ──────
|
|
8
21
|
function resolveCollections(config) {
|
|
9
22
|
if (!config?.collections)
|
|
10
23
|
return [];
|
|
11
24
|
const raw = config.collections;
|
|
12
25
|
const list = Array.isArray(raw) ? raw : Object.values(raw);
|
|
13
26
|
return list
|
|
14
|
-
.filter((c) => !c
|
|
27
|
+
.filter((c) => !c?.admin?.hidden)
|
|
15
28
|
.map((c) => ({ slug: c.slug, type: c.type, labels: c.labels }));
|
|
16
29
|
}
|
|
17
30
|
function collectionLabel(col, plural = true) {
|
|
31
|
+
const fallback = col.slug.charAt(0).toUpperCase() + col.slug.slice(1);
|
|
18
32
|
if (plural)
|
|
19
|
-
return col.labels?.plural ??
|
|
20
|
-
return col.labels?.singular ??
|
|
33
|
+
return col.labels?.plural ?? fallback;
|
|
34
|
+
return col.labels?.singular ?? fallback;
|
|
21
35
|
}
|
|
22
36
|
function relativeTime(dateStr) {
|
|
23
|
-
const now = Date.now();
|
|
24
37
|
const then = new Date(dateStr).getTime();
|
|
25
|
-
const diff = now - then;
|
|
26
|
-
|
|
27
|
-
if (mins < 1)
|
|
38
|
+
const diff = Date.now() - then;
|
|
39
|
+
if (diff < 60_000)
|
|
28
40
|
return 'just now';
|
|
41
|
+
const mins = Math.floor(diff / 60_000);
|
|
29
42
|
if (mins < 60)
|
|
30
|
-
return `${mins}
|
|
43
|
+
return `${mins}m ago`;
|
|
31
44
|
const hours = Math.floor(mins / 60);
|
|
32
45
|
if (hours < 24)
|
|
33
|
-
return `${hours}
|
|
46
|
+
return `${hours}h ago`;
|
|
34
47
|
const days = Math.floor(hours / 24);
|
|
48
|
+
if (days === 1)
|
|
49
|
+
return 'Yesterday';
|
|
35
50
|
if (days < 30)
|
|
36
|
-
return `${days}
|
|
51
|
+
return `${days}d ago`;
|
|
37
52
|
return new Date(dateStr).toLocaleDateString();
|
|
38
53
|
}
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return 'bg-purple-100 text-purple-800';
|
|
47
|
-
case 'IN_REVIEW':
|
|
48
|
-
return 'bg-blue-100 text-blue-800';
|
|
49
|
-
default:
|
|
50
|
-
return 'bg-gray-100 text-gray-700';
|
|
51
|
-
}
|
|
54
|
+
function timeOfDayGreeting() {
|
|
55
|
+
const h = new Date().getHours();
|
|
56
|
+
if (h < 12)
|
|
57
|
+
return 'Good morning';
|
|
58
|
+
if (h < 17)
|
|
59
|
+
return 'Good afternoon';
|
|
60
|
+
return 'Good evening';
|
|
52
61
|
}
|
|
53
|
-
function
|
|
62
|
+
function todayDateString() {
|
|
63
|
+
return new Date().toLocaleDateString('en-US', {
|
|
64
|
+
weekday: 'long',
|
|
65
|
+
month: 'long',
|
|
66
|
+
day: 'numeric',
|
|
67
|
+
year: 'numeric',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// Deterministic colour per author so avatars stay stable across renders
|
|
71
|
+
// without storing anything in state or hitting the server.
|
|
72
|
+
const AVATAR_COLORS = [
|
|
73
|
+
'#7C3AED', // purple
|
|
74
|
+
'#E11D48', // rose
|
|
75
|
+
'#059669', // emerald
|
|
76
|
+
'#0891B2', // cyan
|
|
77
|
+
'#D97706', // amber
|
|
78
|
+
'#4F46E5', // indigo
|
|
79
|
+
'#DC2626', // red
|
|
80
|
+
];
|
|
81
|
+
function hashString(s) {
|
|
82
|
+
let h = 0;
|
|
83
|
+
for (let i = 0; i < s.length; i++)
|
|
84
|
+
h = (h * 31 + s.charCodeAt(i)) | 0;
|
|
85
|
+
return Math.abs(h);
|
|
86
|
+
}
|
|
87
|
+
function authorAvatar(name) {
|
|
88
|
+
const safe = (name ?? '').trim() || 'User';
|
|
89
|
+
const parts = safe.split(/\s+/).filter(Boolean);
|
|
90
|
+
const initials = parts.length >= 2
|
|
91
|
+
? `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase()
|
|
92
|
+
: safe.slice(0, 2).toUpperCase();
|
|
93
|
+
const color = AVATAR_COLORS[hashString(safe) % AVATAR_COLORS.length];
|
|
94
|
+
return { color, initials };
|
|
95
|
+
}
|
|
96
|
+
function statusBadge(status) {
|
|
54
97
|
switch (status) {
|
|
55
98
|
case 'PUBLISHED':
|
|
56
|
-
return
|
|
99
|
+
return {
|
|
100
|
+
label: 'Published',
|
|
101
|
+
cls: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950/60 dark:text-emerald-300',
|
|
102
|
+
};
|
|
57
103
|
case 'DRAFT':
|
|
58
|
-
return
|
|
104
|
+
return {
|
|
105
|
+
label: 'Draft',
|
|
106
|
+
cls: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300',
|
|
107
|
+
};
|
|
59
108
|
case 'SCHEDULED':
|
|
60
|
-
return
|
|
109
|
+
return {
|
|
110
|
+
label: 'Scheduled',
|
|
111
|
+
cls: 'bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-300',
|
|
112
|
+
};
|
|
61
113
|
case 'IN_REVIEW':
|
|
62
|
-
return
|
|
114
|
+
return {
|
|
115
|
+
label: 'In Review',
|
|
116
|
+
cls: 'bg-blue-100 text-blue-800 dark:bg-blue-950/60 dark:text-blue-300',
|
|
117
|
+
};
|
|
63
118
|
default:
|
|
64
|
-
return status;
|
|
119
|
+
return { label: status, cls: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300' };
|
|
65
120
|
}
|
|
66
121
|
}
|
|
67
|
-
|
|
68
|
-
{ bg: 'bg-blue-50', icon: 'bg-blue-100', text: 'text-blue-600' },
|
|
69
|
-
{ bg: 'bg-purple-50', icon: 'bg-purple-100', text: 'text-purple-600' },
|
|
70
|
-
{ bg: 'bg-teal-50', icon: 'bg-teal-100', text: 'text-teal-600' },
|
|
71
|
-
{ bg: 'bg-green-50', icon: 'bg-green-100', text: 'text-green-600' },
|
|
72
|
-
{ bg: 'bg-amber-50', icon: 'bg-amber-100', text: 'text-amber-600' },
|
|
73
|
-
];
|
|
122
|
+
// ─── Dashboard ───────────────────────────────────────────────────────────────
|
|
74
123
|
export function Dashboard({ config, session, onNavigate }) {
|
|
75
124
|
const nav = (path) => onNavigate?.(path);
|
|
76
|
-
const { data, loading, error, exhausted, refetch } = useApiData('/stats');
|
|
125
|
+
const { data: stats, loading, error, exhausted, refetch } = useApiData('/stats');
|
|
77
126
|
const { data: health } = useApiData('/health');
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
const userName = session?.name ?? session?.email?.split('@')[0] ?? '
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
127
|
+
const greeting = useMemo(() => timeOfDayGreeting(), []);
|
|
128
|
+
const dateStr = useMemo(() => todayDateString(), []);
|
|
129
|
+
const userName = session?.name ?? session?.email?.split('@')[0] ?? 'there';
|
|
130
|
+
const collections = useMemo(() => resolveCollections(config), [config]);
|
|
131
|
+
// ── Stat cards ──────────────────────────────────────────────────────────
|
|
132
|
+
const statCards = useMemo(() => {
|
|
133
|
+
const counts = stats?.collectionCounts ?? {};
|
|
134
|
+
// Prefer two real collections (the user's primary content types) over
|
|
135
|
+
// hard-coded Posts/Pages so the dashboard adapts to admin-managed types.
|
|
136
|
+
const primary = [];
|
|
137
|
+
const posts = collections.find((c) => c.slug === 'posts' || c.type === 'post');
|
|
138
|
+
const pages = collections.find((c) => c.slug === 'pages' || c.type === 'page');
|
|
139
|
+
if (posts)
|
|
140
|
+
primary.push(posts);
|
|
141
|
+
if (pages)
|
|
142
|
+
primary.push(pages);
|
|
143
|
+
if (primary.length < 2) {
|
|
144
|
+
for (const c of collections) {
|
|
145
|
+
if (primary.length >= 2)
|
|
146
|
+
break;
|
|
147
|
+
if (!primary.includes(c))
|
|
148
|
+
primary.push(c);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const cards = [];
|
|
152
|
+
if (primary[0]) {
|
|
153
|
+
cards.push({
|
|
154
|
+
label: collectionLabel(primary[0]),
|
|
155
|
+
value: String(counts[primary[0].slug] ?? 0),
|
|
156
|
+
icon: primary[0].type === 'page' ? FileIcon : FileText,
|
|
157
|
+
iconBg: 'bg-violet-100 dark:bg-violet-950/60',
|
|
158
|
+
iconColor: 'text-violet-600 dark:text-violet-300',
|
|
159
|
+
href: `/${primary[0].slug}`,
|
|
94
160
|
});
|
|
95
161
|
}
|
|
162
|
+
else {
|
|
163
|
+
cards.push({
|
|
164
|
+
label: 'Documents',
|
|
165
|
+
value: String(stats?.totalDocuments ?? 0),
|
|
166
|
+
icon: FileText,
|
|
167
|
+
iconBg: 'bg-violet-100 dark:bg-violet-950/60',
|
|
168
|
+
iconColor: 'text-violet-600 dark:text-violet-300',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (primary[1]) {
|
|
172
|
+
cards.push({
|
|
173
|
+
label: collectionLabel(primary[1]),
|
|
174
|
+
value: String(counts[primary[1].slug] ?? 0),
|
|
175
|
+
icon: primary[1].type === 'page' ? FileIcon : FileText,
|
|
176
|
+
iconBg: 'bg-cyan-100 dark:bg-cyan-950/60',
|
|
177
|
+
iconColor: 'text-cyan-600 dark:text-cyan-300',
|
|
178
|
+
href: `/${primary[1].slug}`,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
cards.push({
|
|
182
|
+
label: 'Media',
|
|
183
|
+
value: String(stats?.totalMedia ?? 0),
|
|
184
|
+
hint: stats && stats.totalMedia > 0
|
|
185
|
+
? `${stats.totalMedia} file${stats.totalMedia === 1 ? '' : 's'}`
|
|
186
|
+
: undefined,
|
|
187
|
+
icon: ImageIcon,
|
|
188
|
+
iconBg: 'bg-emerald-100 dark:bg-emerald-950/60',
|
|
189
|
+
iconColor: 'text-emerald-600 dark:text-emerald-300',
|
|
190
|
+
href: '/media',
|
|
191
|
+
});
|
|
192
|
+
// `formCount` in /stats is the *submission* count; the number of
|
|
193
|
+
// forms is the count of `collection: forms` documents.
|
|
194
|
+
const formsCount = counts['forms'] ?? 0;
|
|
195
|
+
const submissions = stats?.formCount ?? 0;
|
|
196
|
+
cards.push({
|
|
197
|
+
label: 'Forms',
|
|
198
|
+
value: String(formsCount),
|
|
199
|
+
hint: submissions > 0 ? `${submissions} response${submissions === 1 ? '' : 's'}` : undefined,
|
|
200
|
+
icon: ClipboardList,
|
|
201
|
+
iconBg: 'bg-amber-100 dark:bg-amber-950/60',
|
|
202
|
+
iconColor: 'text-amber-600 dark:text-amber-300',
|
|
203
|
+
href: '/forms',
|
|
204
|
+
});
|
|
205
|
+
const seo = stats?.avgSeoScore ?? 0;
|
|
206
|
+
cards.push({
|
|
207
|
+
label: 'SEO Score',
|
|
208
|
+
value: seo > 0 ? String(seo) : '—',
|
|
209
|
+
unit: seo > 0 ? '/100' : undefined,
|
|
210
|
+
hint: seo > 0 ? (seo >= 70 ? 'Good' : seo >= 40 ? 'Fair' : 'Needs work') : 'No content yet',
|
|
211
|
+
hintUp: seo >= 70,
|
|
212
|
+
icon: Search,
|
|
213
|
+
iconBg: 'bg-indigo-100 dark:bg-indigo-950/60',
|
|
214
|
+
iconColor: 'text-indigo-600 dark:text-indigo-300',
|
|
215
|
+
href: '/seo',
|
|
216
|
+
});
|
|
217
|
+
return cards;
|
|
218
|
+
}, [stats, collections]);
|
|
219
|
+
// ── Quick actions ───────────────────────────────────────────────────────
|
|
220
|
+
// "New Post" lives only in the hero CTA above; this row is the secondary
|
|
221
|
+
// surface and intentionally omits the primary action to avoid the obvious
|
|
222
|
+
// duplication. Order mirrors the natural authoring flow.
|
|
223
|
+
const quickActions = useMemo(() => {
|
|
224
|
+
const pages = collections.find((c) => c.slug === 'pages' || c.type === 'page');
|
|
225
|
+
const items = [];
|
|
226
|
+
if (pages)
|
|
227
|
+
items.push({ label: 'New Page', icon: Plus, onClick: () => nav(`/${pages.slug}/new`) });
|
|
228
|
+
items.push({ label: 'Upload Media', icon: Upload, onClick: () => nav('/media') });
|
|
229
|
+
items.push({ label: 'New Form', icon: Plus, onClick: () => nav('/forms') });
|
|
230
|
+
items.push({ label: 'Manage SEO', icon: Search, onClick: () => nav('/seo') });
|
|
231
|
+
items.push({ label: 'View API', icon: Globe, onClick: () => nav('/api-keys') });
|
|
232
|
+
return items;
|
|
233
|
+
}, [collections, onNavigate]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
234
|
+
// ── Recent activity ─────────────────────────────────────────────────────
|
|
235
|
+
const [activityLimit, setActivityLimit] = useState(8);
|
|
236
|
+
const activity = useMemo(() => {
|
|
237
|
+
const docs = stats?.recentDocuments ?? [];
|
|
238
|
+
return docs.slice(0, activityLimit).map((d) => {
|
|
239
|
+
const col = collections.find((c) => c.slug === d.collection);
|
|
240
|
+
return {
|
|
241
|
+
...d,
|
|
242
|
+
typeLabel: col ? collectionLabel(col, false) : d.collection,
|
|
243
|
+
relTime: relativeTime(d.updatedAt),
|
|
244
|
+
avatar: authorAvatar(d.author),
|
|
245
|
+
statusInfo: statusBadge(d.status),
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
}, [stats, collections, activityLimit]);
|
|
249
|
+
// ── Publishing queue (scheduled docs only) ──────────────────────────────
|
|
250
|
+
const publishQueue = useMemo(() => {
|
|
251
|
+
const docs = stats?.recentDocuments ?? [];
|
|
252
|
+
return docs
|
|
253
|
+
.filter((d) => d.status === 'SCHEDULED')
|
|
254
|
+
.slice(0, 5)
|
|
255
|
+
.map((d) => {
|
|
256
|
+
const col = collections.find((c) => c.slug === d.collection);
|
|
257
|
+
return {
|
|
258
|
+
id: d.id,
|
|
259
|
+
collection: d.collection,
|
|
260
|
+
title: d.title || 'Untitled',
|
|
261
|
+
type: col ? collectionLabel(col, false) : d.collection,
|
|
262
|
+
date: relativeTime(d.updatedAt),
|
|
263
|
+
author: d.author,
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
}, [stats, collections]);
|
|
267
|
+
// ── Content health summary (derived from /stats) ────────────────────────
|
|
268
|
+
const contentHealth = useMemo(() => {
|
|
269
|
+
const score = stats?.avgSeoScore ?? 0;
|
|
270
|
+
const counts = stats?.statusCounts ?? {};
|
|
271
|
+
const totalDrafts = counts['DRAFT'] ?? 0;
|
|
272
|
+
const totalScheduled = counts['SCHEDULED'] ?? 0;
|
|
273
|
+
const totalInReview = counts['IN_REVIEW'] ?? 0;
|
|
274
|
+
const issues = [];
|
|
275
|
+
if (totalDrafts > 0) {
|
|
276
|
+
issues.push({ label: 'Drafts pending publish', count: totalDrafts, tone: 'warn' });
|
|
277
|
+
}
|
|
278
|
+
if (totalInReview > 0) {
|
|
279
|
+
issues.push({ label: 'Awaiting review', count: totalInReview, tone: 'warn' });
|
|
280
|
+
}
|
|
281
|
+
if (totalScheduled > 0) {
|
|
282
|
+
issues.push({ label: 'Scheduled to publish', count: totalScheduled, tone: 'muted' });
|
|
283
|
+
}
|
|
284
|
+
if (score > 0 && score < 70) {
|
|
285
|
+
issues.push({
|
|
286
|
+
label: 'Pages with weak SEO',
|
|
287
|
+
count: Math.max(1, Math.round((70 - score) / 10)),
|
|
288
|
+
tone: score < 40 ? 'err' : 'warn',
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
score,
|
|
293
|
+
label: score >= 70 ? 'Good' : score >= 40 ? 'Fair' : score > 0 ? 'Poor' : 'No data',
|
|
294
|
+
tone: score >= 70 ? 'ok' : score >= 40 ? 'warn' : score > 0 ? 'err' : 'muted',
|
|
295
|
+
issues,
|
|
296
|
+
};
|
|
297
|
+
}, [stats]);
|
|
298
|
+
// ── Content delivery tiles ──────────────────────────────────────────────
|
|
299
|
+
const delivery = useMemo(() => {
|
|
300
|
+
const sched = stats?.statusCounts?.['SCHEDULED'] ?? 0;
|
|
301
|
+
return {
|
|
302
|
+
totalDocs: stats?.totalDocuments ?? 0,
|
|
303
|
+
forms: stats?.formCount ?? 0,
|
|
304
|
+
scheduled: sched,
|
|
305
|
+
webhooks: {
|
|
306
|
+
total: stats?.webhookCount ?? 0,
|
|
307
|
+
active: stats?.webhookActiveCount ?? 0,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}, [stats]);
|
|
311
|
+
const totalIssues = contentHealth.issues.reduce((s, i) => s + i.count, 0);
|
|
312
|
+
// ── Render ──────────────────────────────────────────────────────────────
|
|
313
|
+
if (loading && !stats) {
|
|
314
|
+
return (_jsx("div", { className: "flex h-64 items-center justify-center p-4 sm:p-6", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-violet-600" }) }));
|
|
96
315
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
statCards.push({ label: 'Forms', value: data?.formCount ?? 0, icon: ClipboardList });
|
|
102
|
-
statCards.push({ label: 'Media', value: data?.totalMedia ?? 0, icon: Image });
|
|
103
|
-
statCards.push({ label: 'Avg. SEO Rating', value: data?.avgSeoScore ?? 0, icon: Search });
|
|
104
|
-
if (loading) {
|
|
105
|
-
return (_jsx("div", { className: "p-4 sm:p-6 flex items-center justify-center h-64", children: _jsx(Loader2, { className: "w-6 h-6 animate-spin text-blue-600" }) }));
|
|
106
|
-
}
|
|
107
|
-
return (_jsxs("div", { className: "p-4 sm:p-6 space-y-6", children: [health && health.status === 'degraded' && (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-blue-200 bg-blue-50 p-3", children: [_jsx(Database, { className: "w-5 h-5 text-blue-600 shrink-0" }), _jsxs("div", { className: "flex-1", children: [_jsx("span", { className: "text-sm font-medium text-blue-900", children: "Database Setup Required" }), _jsx("p", { className: "text-xs text-blue-700 mt-0.5", children: !health.databaseConnected
|
|
316
|
+
const heroPostSlug = collections.find((c) => c.slug === 'posts' || c.type === 'post')?.slug ?? 'posts';
|
|
317
|
+
const siteUrl = config?.site?.url ?? config?.seo?.siteUrl ?? null;
|
|
318
|
+
return (_jsxs("div", { className: "w-full space-y-5 p-4 sm:p-6 lg:px-8", children: [health && health.status === 'degraded' && (_jsxs("div", { className: "flex items-start gap-3 rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-900 dark:bg-blue-950/40", children: [_jsx(Database, { className: "mt-0.5 h-5 w-5 shrink-0 text-blue-600 dark:text-blue-400" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "text-sm font-medium text-blue-900 dark:text-blue-200", children: "Database setup required" }), _jsx("p", { className: "mt-0.5 text-xs text-blue-700 dark:text-blue-300", children: !health.databaseConnected
|
|
108
319
|
? 'Cannot connect to the database. Check your DATABASE_URL environment variable.'
|
|
109
320
|
: !health.secretConfigured
|
|
110
321
|
? 'CMS secret not configured. Set CMS_SECRET or CMS_SESSION_SECRET (min 32 characters).'
|
|
111
322
|
: `Some CMS models are missing: ${Object.entries(health.models)
|
|
112
323
|
.filter(([, v]) => !v)
|
|
113
324
|
.map(([k]) => k)
|
|
114
|
-
.join(', ')}. Run your database migrations.` })] })] })), error && exhausted && (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-amber-200 bg-amber-50 p-3", children: [_jsx(AlertTriangle, { className: "
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
325
|
+
.join(', ')}. Run your database migrations.` })] })] })), error && exhausted && (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-900 dark:bg-amber-950/40", children: [_jsx(AlertTriangle, { className: "h-5 w-5 shrink-0 text-amber-600 dark:text-amber-400" }), _jsx("span", { className: "flex-1 text-sm text-amber-800 dark:text-amber-200", children: "Some dashboard data may be unavailable." }), _jsx("button", { onClick: refetch, className: "rounded-lg border border-amber-300 px-3 py-1 text-sm text-amber-700 transition-colors hover:bg-amber-100 dark:border-amber-700 dark:text-amber-200 dark:hover:bg-amber-900/40", children: "Retry" })] })), _jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("h1", { className: "text-foreground text-xl font-semibold tracking-tight sm:text-2xl", children: [greeting, ", ", userName, " ", _jsx("span", { "aria-hidden": true, children: "\uD83D\uDC4B" })] }), _jsxs("p", { className: "text-muted-foreground mt-0.5 text-sm", children: [dateStr, totalIssues > 0 && (_jsxs("span", { className: "hidden sm:inline", children: [' · ', totalIssues, " content item", totalIssues === 1 ? '' : 's', " need attention"] }))] })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [_jsxs("button", { onClick: () => nav(`/${heroPostSlug}/new`), className: "inline-flex items-center gap-1.5 rounded-lg bg-violet-600 px-3.5 py-2 text-sm font-medium text-white transition-colors hover:bg-violet-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), " New Post"] }), siteUrl && (_jsxs("a", { href: siteUrl, target: "_blank", rel: "noopener noreferrer", className: "border-border bg-card hover:bg-accent hover:text-accent-foreground hidden items-center gap-1.5 rounded-lg border px-3 py-2 text-sm transition-colors sm:inline-flex", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5" }), " View Site"] }))] })] }), _jsx("div", { className: "-mx-1 flex gap-2 overflow-x-auto px-1 pb-1 [scrollbar-width:thin]", children: quickActions.map((a) => (_jsxs("button", { onClick: a.onClick, className: "border-border bg-card inline-flex shrink-0 items-center gap-1.5 rounded-lg border px-3.5 py-2 text-sm shadow-sm transition-colors hover:border-violet-400 hover:bg-violet-50 hover:text-violet-700 dark:hover:bg-violet-950/40 dark:hover:text-violet-300", children: [_jsx(a.icon, { className: "h-3.5 w-3.5" }), a.label] }, a.label))) }), _jsx("div", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5", children: statCards.map((card) => {
|
|
326
|
+
const Inner = (_jsxs(_Fragment, { children: [_jsx("div", { className: `mb-3 flex h-8 w-8 items-center justify-center rounded-lg ${card.iconBg}`, children: _jsx(card.icon, { className: `h-4 w-4 ${card.iconColor}` }) }), _jsxs("div", { className: "flex items-baseline gap-1", children: [_jsx("span", { className: "text-foreground text-2xl leading-none font-semibold tracking-tight", children: card.value }), card.unit && (_jsx("span", { className: "text-muted-foreground text-sm font-medium", children: card.unit }))] }), _jsx("p", { className: "text-muted-foreground mt-1 text-xs", children: card.label }), card.hint && (_jsx("p", { className: `mt-1.5 text-[11px] ${card.hintUp ? 'text-emerald-600 dark:text-emerald-400' : 'text-muted-foreground'}`, children: card.hint }))] }));
|
|
327
|
+
return card.href ? (_jsx("button", { type: "button", onClick: () => nav(card.href), className: "bg-card border-border rounded-xl border p-4 text-left shadow-sm transition-shadow hover:shadow-md", children: Inner }, card.label)) : (_jsx("div", { className: "bg-card border-border rounded-xl border p-4 shadow-sm", children: Inner }, card.label));
|
|
328
|
+
}) }), _jsxs("div", { className: "grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_320px]", children: [_jsxs("section", { className: "bg-card border-border overflow-hidden rounded-xl border shadow-sm", children: [_jsxs("header", { className: "border-border flex items-center justify-between border-b px-4 py-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "text-foreground text-sm font-semibold", children: "Recent Activity" }), _jsx("p", { className: "text-muted-foreground mt-0.5 text-xs", children: "Last 7 days across all content" })] }), _jsx("button", { className: "text-xs font-medium text-violet-600 hover:underline dark:text-violet-400", onClick: () => setActivityLimit((n) => (n >= 20 ? 8 : 20)), children: activity.length >= 20 ? 'Show less' : 'View all' })] }), activity.length === 0 ? (_jsx(EmptyState, { icon: Activity, title: "No activity yet", subtitle: "Content changes will appear here as your team works." })) : (_jsx("ul", { role: "list", className: "divide-border divide-y", children: activity.map((it) => (_jsx("li", { className: "hover:bg-accent/50 cursor-pointer px-4 py-3 transition-colors", onClick: () => nav(`/${it.collection}/${it.id}`), children: _jsxs("div", { className: "flex min-w-0 items-start gap-3", children: [_jsx("div", { className: "mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-bold text-white", style: { background: it.avatar.color }, "aria-hidden": true, children: it.avatar.initials }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("p", { className: "text-foreground truncate text-sm leading-snug", children: [_jsxs("span", { className: "font-semibold", children: ["\u201C", it.title || 'Untitled', "\u201D"] }), ' ', _jsxs("span", { className: "text-muted-foreground", children: ["\u2014 ", it.typeLabel] })] }), _jsxs("div", { className: "text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-[11px]", children: [_jsx("span", { className: `inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${it.statusInfo.cls}`, children: it.statusInfo.label }), _jsx("span", { className: "max-w-[120px] truncate", children: it.author }), _jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx("span", { children: it.relTime })] })] })] }) }, it.id))) }))] }), _jsxs("aside", { className: "flex min-w-0 flex-col gap-4", children: [_jsxs("section", { className: "bg-card border-border overflow-hidden rounded-xl border shadow-sm", children: [_jsxs("header", { className: "border-border flex items-center justify-between border-b px-4 py-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "text-foreground text-sm font-semibold", children: "Publishing Queue" }), _jsxs("p", { className: "text-muted-foreground mt-0.5 text-xs", children: [publishQueue.length, " item", publishQueue.length === 1 ? '' : 's', " scheduled"] })] }), _jsx("button", { className: "text-xs font-medium text-violet-600 hover:underline disabled:cursor-default disabled:no-underline disabled:opacity-50 dark:text-violet-400", disabled: publishQueue.length === 0, onClick: () => {
|
|
329
|
+
// We don't have a dedicated "scheduled" admin page, so deep-
|
|
330
|
+
// link to the first scheduled item — that's where authors
|
|
331
|
+
// typically need to land to reschedule or cancel.
|
|
332
|
+
const first = publishQueue[0];
|
|
333
|
+
if (first)
|
|
334
|
+
nav(`/${first.collection}/${first.id}`);
|
|
335
|
+
}, children: "Manage" })] }), publishQueue.length === 0 ? (_jsx(EmptyState, { icon: Clock, title: "No scheduled content", subtitle: "Scheduled posts and pages appear here.", compact: true })) : (_jsx("ul", { role: "list", className: "divide-border divide-y", children: publishQueue.map((q) => (_jsxs("li", { className: "hover:bg-accent/50 flex min-w-0 cursor-pointer items-center gap-2.5 px-4 py-2.5 transition-colors", onClick: () => nav(`/${q.collection}/${q.id}`), children: [_jsx("span", { className: "h-2 w-2 shrink-0 rounded-full bg-amber-500", "aria-hidden": true }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "text-foreground truncate text-sm font-medium", children: q.title }), _jsxs("p", { className: "text-muted-foreground truncate text-[11px]", children: [q.date, " \u00B7 ", q.author] })] }), _jsx("span", { className: "border-border bg-background text-muted-foreground shrink-0 rounded border px-1.5 py-0.5 text-[10px] capitalize", children: q.type })] }, q.id))) }))] }), _jsxs("section", { className: "bg-card border-border overflow-hidden rounded-xl border shadow-sm", children: [_jsxs("header", { className: "border-border flex items-center justify-between border-b px-4 py-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "text-foreground text-sm font-semibold", children: "Content Health" }), _jsx("p", { className: "text-muted-foreground mt-0.5 text-xs", children: "SEO & quality issues" })] }), _jsx("button", { className: "text-xs font-medium text-violet-600 hover:underline dark:text-violet-400", onClick: () => nav('/seo'), children: "Fix issues" })] }), _jsxs("div", { className: "px-4 pt-3.5 pb-2.5", children: [_jsxs("div", { className: "mb-1.5 flex items-baseline justify-between", children: [_jsxs("p", { className: "text-foreground text-2xl leading-none font-semibold tracking-tight", children: [contentHealth.score > 0 ? contentHealth.score : '—', contentHealth.score > 0 && (_jsx("span", { className: "text-muted-foreground ml-0.5 text-sm font-normal", children: "/100" }))] }), _jsx("span", { className: `text-xs font-medium ${contentHealth.tone === 'ok'
|
|
336
|
+
? 'text-emerald-600 dark:text-emerald-400'
|
|
337
|
+
: contentHealth.tone === 'warn'
|
|
338
|
+
? 'text-amber-600 dark:text-amber-400'
|
|
339
|
+
: contentHealth.tone === 'err'
|
|
340
|
+
? 'text-red-600 dark:text-red-400'
|
|
341
|
+
: 'text-muted-foreground'}`, children: contentHealth.label })] }), _jsx("div", { className: "bg-muted h-1.5 overflow-hidden rounded-full", children: _jsx("div", { className: `h-full rounded-full transition-all ${contentHealth.tone === 'ok'
|
|
342
|
+
? 'bg-emerald-500'
|
|
343
|
+
: contentHealth.tone === 'warn'
|
|
344
|
+
? 'bg-amber-500'
|
|
345
|
+
: contentHealth.tone === 'err'
|
|
346
|
+
? 'bg-red-500'
|
|
347
|
+
: 'bg-muted-foreground/40'}`, style: { width: `${Math.max(0, Math.min(100, contentHealth.score))}%` } }) })] }), contentHealth.issues.length === 0 ? (_jsx("div", { className: "text-muted-foreground px-4 pt-1 pb-4 text-xs", children: "No outstanding issues. Nice work." })) : (_jsx("ul", { role: "list", className: "divide-border divide-y", children: contentHealth.issues.map((iss, i) => (_jsxs("li", { className: "hover:bg-accent/50 flex cursor-pointer items-center gap-2.5 px-4 py-2 transition-colors", onClick: () => nav('/seo'), children: [_jsx("span", { className: `h-1.5 w-1.5 shrink-0 rounded-full ${iss.tone === 'err'
|
|
348
|
+
? 'bg-red-500'
|
|
349
|
+
: iss.tone === 'warn'
|
|
350
|
+
? 'bg-amber-500'
|
|
351
|
+
: 'bg-muted-foreground'}`, "aria-hidden": true }), _jsx("span", { className: "text-muted-foreground flex-1 truncate text-xs", children: iss.label }), _jsx("span", { className: `shrink-0 text-sm font-semibold ${iss.tone === 'err'
|
|
352
|
+
? 'text-red-600 dark:text-red-400'
|
|
353
|
+
: iss.tone === 'warn'
|
|
354
|
+
? 'text-amber-600 dark:text-amber-400'
|
|
355
|
+
: 'text-muted-foreground'}`, children: iss.count }), _jsx(ChevronRight, { className: "text-muted-foreground/60 h-3.5 w-3.5 shrink-0" })] }, i))) }))] })] })] }), _jsxs("section", { className: "bg-card border-border overflow-hidden rounded-xl border shadow-sm", children: [_jsxs("header", { className: "border-border flex items-center justify-between border-b px-4 py-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "text-foreground text-sm font-semibold", children: "Content Delivery" }), _jsx("p", { className: "text-muted-foreground mt-0.5 text-xs", children: "Real-time platform activity" })] }), _jsxs("button", { className: "inline-flex items-center gap-1 text-xs font-medium text-violet-600 hover:underline dark:text-violet-400", onClick: () => nav('/api-keys'), children: ["API docs ", _jsx(ExternalLink, { className: "h-3 w-3" })] })] }), _jsxs("div", { className: "divide-border grid grid-cols-2 divide-x divide-y lg:grid-cols-4 lg:divide-y-0", children: [_jsx(DeliveryTile, { icon: Activity, label: "Total Documents", value: delivery.totalDocs.toLocaleString(), sub: "across all collections" }), _jsx(DeliveryTile, { icon: Clock, label: "Scheduled Posts", value: delivery.scheduled.toLocaleString(), sub: delivery.scheduled > 0 ? 'in publishing queue' : 'none queued', tone: delivery.scheduled > 0 ? 'ok' : 'muted' }), _jsx(DeliveryTile, { icon: ClipboardList, label: "Form Responses", value: delivery.forms.toLocaleString(), sub: delivery.forms > 0 ? 'total received' : 'none yet', tone: delivery.forms > 0 ? 'ok' : 'muted' }), _jsx(DeliveryTile, { icon: Zap, label: "Active Webhooks", value: `${delivery.webhooks.active} / ${delivery.webhooks.total || 0}`, sub: delivery.webhooks.total === 0
|
|
356
|
+
? 'none configured'
|
|
357
|
+
: delivery.webhooks.active === delivery.webhooks.total
|
|
358
|
+
? 'all active'
|
|
359
|
+
: `${delivery.webhooks.total - delivery.webhooks.active} paused`, tone: delivery.webhooks.total === 0
|
|
360
|
+
? 'muted'
|
|
361
|
+
: delivery.webhooks.active === delivery.webhooks.total
|
|
362
|
+
? 'ok'
|
|
363
|
+
: 'warn' })] })] })] }));
|
|
364
|
+
}
|
|
365
|
+
// ─── Sub-components (kept in-file: smaller bundle, no extra module hops) ────
|
|
366
|
+
function EmptyState({ icon: Icon, title, subtitle, compact = false, }) {
|
|
367
|
+
return (_jsxs("div", { className: `flex flex-col items-center justify-center gap-1.5 text-center ${compact ? 'px-4 py-6' : 'px-6 py-10'}`, children: [_jsx(Icon, { className: "text-muted-foreground/50 mb-1 h-7 w-7", "aria-hidden": true }), _jsx("p", { className: "text-foreground text-sm font-semibold", children: title }), _jsx("p", { className: "text-muted-foreground max-w-xs text-xs", children: subtitle })] }));
|
|
368
|
+
}
|
|
369
|
+
function DeliveryTile({ icon: Icon, label, value, sub, tone = 'muted', }) {
|
|
370
|
+
const subTone = tone === 'ok'
|
|
371
|
+
? 'text-emerald-600 dark:text-emerald-400'
|
|
372
|
+
: tone === 'warn'
|
|
373
|
+
? 'text-amber-600 dark:text-amber-400'
|
|
374
|
+
: tone === 'err'
|
|
375
|
+
? 'text-red-600 dark:text-red-400'
|
|
376
|
+
: 'text-muted-foreground';
|
|
377
|
+
return (_jsxs("div", { className: "px-4 py-3.5", children: [_jsxs("div", { className: "text-muted-foreground mb-1 flex items-center gap-1.5", children: [_jsx(Icon, { className: "h-3.5 w-3.5", "aria-hidden": true }), _jsx("span", { className: "text-[11px] font-medium", children: label })] }), _jsx("p", { className: "text-foreground text-xl leading-tight font-semibold tracking-tight", children: value }), sub && _jsx("p", { className: `mt-0.5 text-[11px] ${subTone}`, children: sub })] }));
|
|
123
378
|
}
|
|
124
379
|
//# sourceMappingURL=Dashboard.js.map
|