@actuate-media/cms-admin 0.65.0 → 0.67.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/CHANGELOG.md +39 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +7 -4
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/components/inspector-pane.test.d.ts +2 -0
- package/dist/__tests__/components/inspector-pane.test.d.ts.map +1 -0
- package/dist/__tests__/components/inspector-pane.test.js +31 -0
- package/dist/__tests__/components/inspector-pane.test.js.map +1 -0
- package/dist/__tests__/lib/pages-tree.test.d.ts +2 -0
- package/dist/__tests__/lib/pages-tree.test.d.ts.map +1 -0
- package/dist/__tests__/lib/pages-tree.test.js +173 -0
- package/dist/__tests__/lib/pages-tree.test.js.map +1 -0
- package/dist/__tests__/views/dashboard.test.js +7 -3
- package/dist/__tests__/views/dashboard.test.js.map +1 -1
- package/dist/__tests__/views/media-seo-audit.render.test.d.ts +2 -0
- package/dist/__tests__/views/media-seo-audit.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/media-seo-audit.render.test.js +102 -0
- package/dist/__tests__/views/media-seo-audit.render.test.js.map +1 -0
- package/dist/__tests__/views/onboarding-block.render.test.d.ts +2 -0
- package/dist/__tests__/views/onboarding-block.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/onboarding-block.render.test.js +146 -0
- package/dist/__tests__/views/onboarding-block.render.test.js.map +1 -0
- package/dist/__tests__/views/pages-list-view.test.js +77 -1
- package/dist/__tests__/views/pages-list-view.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/script-tags/ScriptTagEditorPane.d.ts +26 -0
- package/dist/components/script-tags/ScriptTagEditorPane.d.ts.map +1 -0
- package/dist/components/script-tags/ScriptTagEditorPane.js +160 -0
- package/dist/components/script-tags/ScriptTagEditorPane.js.map +1 -0
- package/dist/components/ui/InspectorPane.d.ts +51 -0
- package/dist/components/ui/InspectorPane.d.ts.map +1 -0
- package/dist/components/ui/InspectorPane.js +42 -0
- package/dist/components/ui/InspectorPane.js.map +1 -0
- package/dist/components/ui/RowActions.d.ts +19 -0
- package/dist/components/ui/RowActions.d.ts.map +1 -0
- package/dist/components/ui/RowActions.js +16 -0
- package/dist/components/ui/RowActions.js.map +1 -0
- package/dist/components/ui/index.d.ts +4 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +2 -0
- package/dist/components/ui/index.js.map +1 -1
- package/dist/layout/Header.js +1 -1
- package/dist/layout/Header.js.map +1 -1
- package/dist/layout/Layout.js +8 -38
- package/dist/layout/Layout.js.map +1 -1
- package/dist/lib/page-editor-service.d.ts +11 -2
- package/dist/lib/page-editor-service.d.ts.map +1 -1
- package/dist/lib/page-editor-service.js +8 -2
- package/dist/lib/page-editor-service.js.map +1 -1
- package/dist/lib/pages-service.d.ts +9 -0
- package/dist/lib/pages-service.d.ts.map +1 -1
- package/dist/lib/pages-service.js +14 -0
- package/dist/lib/pages-service.js.map +1 -1
- package/dist/lib/pages-tree.d.ts +44 -0
- package/dist/lib/pages-tree.d.ts.map +1 -0
- package/dist/lib/pages-tree.js +106 -0
- package/dist/lib/pages-tree.js.map +1 -0
- package/dist/lib/posts-service.d.ts +11 -0
- package/dist/lib/posts-service.d.ts.map +1 -1
- package/dist/lib/posts-service.js +13 -0
- package/dist/lib/posts-service.js.map +1 -1
- package/dist/lib/seo-service.d.ts +3 -0
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +40 -3
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +55 -2
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/MediaSeoAudit.d.ts +25 -0
- package/dist/views/MediaSeoAudit.d.ts.map +1 -0
- package/dist/views/MediaSeoAudit.js +97 -0
- package/dist/views/MediaSeoAudit.js.map +1 -0
- package/dist/views/Pages/PagesListView.d.ts.map +1 -1
- package/dist/views/Pages/PagesListView.js +145 -16
- package/dist/views/Pages/PagesListView.js.map +1 -1
- package/dist/views/Pages/PagesTreeTable.d.ts +35 -0
- package/dist/views/Pages/PagesTreeTable.d.ts.map +1 -0
- package/dist/views/Pages/PagesTreeTable.js +28 -0
- package/dist/views/Pages/PagesTreeTable.js.map +1 -0
- package/dist/views/Posts/PostsListView.d.ts.map +1 -1
- package/dist/views/Posts/PostsListView.js +66 -9
- package/dist/views/Posts/PostsListView.js.map +1 -1
- package/dist/views/ScriptTags.d.ts +5 -1
- package/dist/views/ScriptTags.d.ts.map +1 -1
- package/dist/views/ScriptTags.js +32 -7
- package/dist/views/ScriptTags.js.map +1 -1
- package/dist/views/dashboard/OnboardingBlock.d.ts +29 -0
- package/dist/views/dashboard/OnboardingBlock.d.ts.map +1 -0
- package/dist/views/dashboard/OnboardingBlock.js +71 -0
- package/dist/views/dashboard/OnboardingBlock.js.map +1 -0
- package/dist/views/page-editor/PageSectionEditor.d.ts +7 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +3 -3
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/PageSettingsModal.d.ts +1 -1
- package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -1
- package/dist/views/page-editor/PageSettingsModal.js +42 -3
- package/dist/views/page-editor/PageSettingsModal.js.map +1 -1
- package/dist/views/seo/TechnicalTab.d.ts.map +1 -1
- package/dist/views/seo/TechnicalTab.js +15 -0
- package/dist/views/seo/TechnicalTab.js.map +1 -1
- package/package.json +3 -3
- package/src/AdminRoot.tsx +7 -3
- package/src/__tests__/components/inspector-pane.test.tsx +64 -0
- package/src/__tests__/lib/pages-tree.test.ts +197 -0
- package/src/__tests__/views/dashboard.test.tsx +7 -3
- package/src/__tests__/views/media-seo-audit.render.test.tsx +117 -0
- package/src/__tests__/views/onboarding-block.render.test.tsx +193 -0
- package/src/__tests__/views/pages-list-view.test.tsx +93 -1
- package/src/components/script-tags/ScriptTagEditorPane.tsx +474 -0
- package/src/components/ui/InspectorPane.tsx +129 -0
- package/src/components/ui/RowActions.tsx +50 -0
- package/src/components/ui/index.ts +12 -0
- package/src/layout/Header.tsx +2 -2
- package/src/layout/Layout.tsx +8 -80
- package/src/lib/page-editor-service.ts +16 -2
- package/src/lib/pages-service.ts +18 -0
- package/src/lib/pages-tree.ts +145 -0
- package/src/lib/posts-service.ts +26 -0
- package/src/lib/seo-service.ts +3 -0
- package/src/views/Dashboard.tsx +286 -214
- package/src/views/MediaBrowser.tsx +673 -512
- package/src/views/MediaSeoAudit.tsx +320 -0
- package/src/views/Pages/PagesListView.tsx +459 -185
- package/src/views/Pages/PagesTreeTable.tsx +339 -0
- package/src/views/Posts/PostsListView.tsx +135 -10
- package/src/views/ScriptTags.tsx +204 -129
- package/src/views/dashboard/OnboardingBlock.tsx +233 -0
- package/src/views/page-editor/PageSectionEditor.tsx +9 -2
- package/src/views/page-editor/PageSettingsModal.tsx +75 -2
- package/src/views/seo/TechnicalTab.tsx +15 -0
- package/dist/views/ScriptTagEditor.d.ts +0 -6
- package/dist/views/ScriptTagEditor.d.ts.map +0 -1
- package/dist/views/ScriptTagEditor.js +0 -111
- package/dist/views/ScriptTagEditor.js.map +0 -1
- package/src/views/ScriptTagEditor.tsx +0 -353
package/src/views/ScriptTags.tsx
CHANGED
|
@@ -5,30 +5,22 @@ import { Code2, Plus, Loader2, AlertTriangle } from 'lucide-react'
|
|
|
5
5
|
import { toast } from 'sonner'
|
|
6
6
|
import { EnabledSwitch } from '../components/EnabledSwitch.js'
|
|
7
7
|
import { StatPill } from '../components/ui/StatPill.js'
|
|
8
|
+
import { InspectorPane, SplitPaneLayout, SplitPaneList } from '../components/ui/InspectorPane.js'
|
|
9
|
+
import {
|
|
10
|
+
CONSENT_LABELS,
|
|
11
|
+
PLACEMENT_LABELS,
|
|
12
|
+
ScriptTagEditorPane,
|
|
13
|
+
type ScriptTag,
|
|
14
|
+
} from '../components/script-tags/ScriptTagEditorPane.js'
|
|
8
15
|
import { useApiData } from '../lib/useApiData.js'
|
|
9
16
|
import { cmsApi } from '../lib/api.js'
|
|
10
17
|
|
|
11
|
-
interface ScriptTag {
|
|
12
|
-
id: string
|
|
13
|
-
name: string
|
|
14
|
-
code: string
|
|
15
|
-
placement: string
|
|
16
|
-
scope: string
|
|
17
|
-
targetPaths: string[]
|
|
18
|
-
priority: number
|
|
19
|
-
enabled: boolean
|
|
20
|
-
createdAt: string
|
|
21
|
-
updatedAt: string
|
|
22
|
-
}
|
|
23
|
-
|
|
24
18
|
export interface ScriptTagsProps {
|
|
25
19
|
onNavigate?: (path: string) => void
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
body_open: 'Body Open',
|
|
31
|
-
body_close: 'Body Close',
|
|
20
|
+
/** Deep-link support: open this tag's editor pane on mount (/script-tags/:id). */
|
|
21
|
+
initialSelectedId?: string
|
|
22
|
+
/** Deep-link support: open the create pane on mount (/script-tags/new). */
|
|
23
|
+
initialNew?: boolean
|
|
32
24
|
}
|
|
33
25
|
|
|
34
26
|
const PLACEMENT_COLORS: Record<string, string> = {
|
|
@@ -59,10 +51,25 @@ function scopeLabel(tag: ScriptTag): string {
|
|
|
59
51
|
return scope || 'Custom'
|
|
60
52
|
}
|
|
61
53
|
|
|
62
|
-
|
|
54
|
+
function formatUpdated(value: string): string {
|
|
55
|
+
const date = new Date(value)
|
|
56
|
+
if (Number.isNaN(date.getTime())) return '—'
|
|
57
|
+
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type Selection = { mode: 'edit'; id: string } | { mode: 'new' } | null
|
|
61
|
+
|
|
62
|
+
export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
|
|
63
63
|
const { data, loading, error, refetch } = useApiData<ScriptTag[]>('/script-tags')
|
|
64
64
|
const [togglingId, setTogglingId] = useState<string | null>(null)
|
|
65
65
|
const [enabledOverrides, setEnabledOverrides] = useState<Record<string, boolean>>({})
|
|
66
|
+
const [selection, setSelection] = useState<Selection>(
|
|
67
|
+
initialNew
|
|
68
|
+
? { mode: 'new' }
|
|
69
|
+
: initialSelectedId
|
|
70
|
+
? { mode: 'edit', id: initialSelectedId }
|
|
71
|
+
: null,
|
|
72
|
+
)
|
|
66
73
|
|
|
67
74
|
const tags = useMemo(
|
|
68
75
|
() =>
|
|
@@ -73,6 +80,16 @@ export function ScriptTags({ onNavigate }: ScriptTagsProps) {
|
|
|
73
80
|
[data, enabledOverrides],
|
|
74
81
|
)
|
|
75
82
|
|
|
83
|
+
const selectedTag =
|
|
84
|
+
selection?.mode === 'edit' ? (tags.find((t) => t.id === selection.id) ?? null) : null
|
|
85
|
+
const paneOpen = selection?.mode === 'new' || selectedTag !== null
|
|
86
|
+
|
|
87
|
+
const toggleRow = (tag: ScriptTag) => {
|
|
88
|
+
setSelection((prev) =>
|
|
89
|
+
prev?.mode === 'edit' && prev.id === tag.id ? null : { mode: 'edit', id: tag.id },
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
76
93
|
const toggleEnabled = async (tag: ScriptTag, nextEnabled: boolean) => {
|
|
77
94
|
setEnabledOverrides((prev) => ({ ...prev, [tag.id]: nextEnabled }))
|
|
78
95
|
setTogglingId(tag.id)
|
|
@@ -99,6 +116,12 @@ export function ScriptTags({ onNavigate }: ScriptTagsProps) {
|
|
|
99
116
|
toast.success(saved ? 'Tag enabled' : 'Tag disabled')
|
|
100
117
|
}
|
|
101
118
|
|
|
119
|
+
const handlePaneChanged = (savedId?: string) => {
|
|
120
|
+
setEnabledOverrides({})
|
|
121
|
+
refetch()
|
|
122
|
+
if (savedId) setSelection({ mode: 'edit', id: savedId })
|
|
123
|
+
}
|
|
124
|
+
|
|
102
125
|
if (loading) {
|
|
103
126
|
return (
|
|
104
127
|
<div className="flex h-64 items-center justify-center p-3 pr-6 sm:p-4 sm:pr-8">
|
|
@@ -108,122 +131,174 @@ export function ScriptTags({ onNavigate }: ScriptTagsProps) {
|
|
|
108
131
|
}
|
|
109
132
|
|
|
110
133
|
return (
|
|
111
|
-
<div className="
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
<div className="mb-4 flex items-center justify-between">
|
|
126
|
-
<div>
|
|
127
|
-
<h1 className="text-foreground mb-1">Script Tags</h1>
|
|
128
|
-
<p className="text-muted-foreground text-sm">
|
|
129
|
-
Manage tracking codes, analytics, and custom scripts injected into your site
|
|
130
|
-
</p>
|
|
131
|
-
{tags.length > 0 && (
|
|
132
|
-
<div className="mt-3 flex flex-wrap gap-1.5">
|
|
133
|
-
<StatPill value={tags.length} label="Total" />
|
|
134
|
-
<StatPill
|
|
135
|
-
value={tags.filter((t) => t.enabled).length}
|
|
136
|
-
label="Active"
|
|
137
|
-
valueClassName="text-[var(--suc)]"
|
|
138
|
-
/>
|
|
139
|
-
<StatPill
|
|
140
|
-
value={tags.filter((t) => !t.enabled).length}
|
|
141
|
-
label="Inactive"
|
|
142
|
-
valueClassName="text-[var(--muted)]"
|
|
143
|
-
/>
|
|
144
|
-
</div>
|
|
145
|
-
)}
|
|
146
|
-
</div>
|
|
147
|
-
<button
|
|
148
|
-
type="button"
|
|
149
|
-
onClick={() => onNavigate?.('/script-tags/new')}
|
|
150
|
-
className="bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
|
|
151
|
-
>
|
|
152
|
-
<Plus className="h-4 w-4" />
|
|
153
|
-
New Tag
|
|
154
|
-
</button>
|
|
155
|
-
</div>
|
|
134
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
135
|
+
<div className="p-3 pr-6 pb-0 sm:p-4 sm:pr-8 sm:pb-0">
|
|
136
|
+
{error && (
|
|
137
|
+
<div className="border-destructive/30 bg-destructive/10 mb-4 flex items-center gap-3 rounded-lg border p-3">
|
|
138
|
+
<AlertTriangle className="text-destructive h-5 w-5 shrink-0" />
|
|
139
|
+
<span className="text-destructive flex-1 text-sm">{error}</span>
|
|
140
|
+
<button
|
|
141
|
+
onClick={refetch}
|
|
142
|
+
className="border-destructive/40 text-destructive hover:bg-destructive/15 rounded-lg border px-3 py-1 text-sm transition-colors"
|
|
143
|
+
>
|
|
144
|
+
Retry
|
|
145
|
+
</button>
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
156
148
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
149
|
+
<div className="mb-4 flex items-center justify-between">
|
|
150
|
+
<div>
|
|
151
|
+
<h1 className="text-foreground mb-1">Script Tags</h1>
|
|
152
|
+
<p className="text-muted-foreground text-sm">
|
|
153
|
+
Manage tracking codes, analytics, and custom scripts injected into your site
|
|
154
|
+
</p>
|
|
155
|
+
{tags.length > 0 && (
|
|
156
|
+
<div className="mt-3 flex flex-wrap gap-1.5">
|
|
157
|
+
<StatPill value={tags.length} label="Total" />
|
|
158
|
+
<StatPill
|
|
159
|
+
value={tags.filter((t) => t.enabled).length}
|
|
160
|
+
label="Active"
|
|
161
|
+
valueClassName="text-[var(--suc)]"
|
|
162
|
+
/>
|
|
163
|
+
<StatPill
|
|
164
|
+
value={tags.filter((t) => !t.enabled).length}
|
|
165
|
+
label="Inactive"
|
|
166
|
+
valueClassName="text-[var(--muted)]"
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
164
171
|
<button
|
|
165
172
|
type="button"
|
|
166
|
-
onClick={() =>
|
|
167
|
-
className="bg-primary text-primary-foreground hover:bg-primary/90
|
|
173
|
+
onClick={() => setSelection({ mode: 'new' })}
|
|
174
|
+
className="bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
|
|
168
175
|
>
|
|
169
176
|
<Plus className="h-4 w-4" />
|
|
170
|
-
|
|
177
|
+
New Script
|
|
171
178
|
</button>
|
|
172
179
|
</div>
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
<
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
>
|
|
199
|
-
|
|
200
|
-
</
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
</
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<SplitPaneLayout>
|
|
183
|
+
<SplitPaneList className="p-3 pt-0 pr-6 sm:p-4 sm:pt-0 sm:pr-8">
|
|
184
|
+
{tags.length === 0 && !error ? (
|
|
185
|
+
<div className="border-border bg-card rounded-lg border p-12 text-center">
|
|
186
|
+
<Code2 className="text-muted-foreground mx-auto mb-3 h-10 w-10" />
|
|
187
|
+
<h3 className="text-foreground text-sm font-medium">No script tags yet</h3>
|
|
188
|
+
<p className="text-muted-foreground mt-1 text-sm">
|
|
189
|
+
Add tracking codes like Google Analytics, Tag Manager, or Facebook Pixel.
|
|
190
|
+
</p>
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
onClick={() => setSelection({ mode: 'new' })}
|
|
194
|
+
className="bg-primary text-primary-foreground hover:bg-primary/90 mt-4 inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
|
|
195
|
+
>
|
|
196
|
+
<Plus className="h-4 w-4" />
|
|
197
|
+
Add Your First Script
|
|
198
|
+
</button>
|
|
199
|
+
</div>
|
|
200
|
+
) : (
|
|
201
|
+
<div className="border-border bg-card overflow-hidden rounded-lg border">
|
|
202
|
+
<table className="w-full text-sm" aria-label="Script tags">
|
|
203
|
+
<thead>
|
|
204
|
+
<tr className="border-border bg-muted border-b">
|
|
205
|
+
<th className="text-muted-foreground px-4 py-3 text-left font-medium">
|
|
206
|
+
Script
|
|
207
|
+
</th>
|
|
208
|
+
<th className="text-muted-foreground px-4 py-3 text-left font-medium">
|
|
209
|
+
Location
|
|
210
|
+
</th>
|
|
211
|
+
<th className="text-muted-foreground px-4 py-3 text-center font-medium">
|
|
212
|
+
Priority
|
|
213
|
+
</th>
|
|
214
|
+
<th className="text-muted-foreground px-4 py-3 text-left font-medium">Scope</th>
|
|
215
|
+
<th className="text-muted-foreground px-4 py-3 text-left font-medium">
|
|
216
|
+
Consent
|
|
217
|
+
</th>
|
|
218
|
+
<th className="text-muted-foreground px-4 py-3 text-center font-medium">
|
|
219
|
+
Active
|
|
220
|
+
</th>
|
|
221
|
+
<th className="text-muted-foreground px-4 py-3 text-left font-medium">
|
|
222
|
+
Updated
|
|
223
|
+
</th>
|
|
224
|
+
</tr>
|
|
225
|
+
</thead>
|
|
226
|
+
<tbody>
|
|
227
|
+
{tags.map((tag) => {
|
|
228
|
+
const isSelected = selection?.mode === 'edit' && selection.id === tag.id
|
|
229
|
+
return (
|
|
230
|
+
<tr
|
|
231
|
+
key={tag.id}
|
|
232
|
+
className={`border-border cursor-pointer border-b transition-colors last:border-0 ${
|
|
233
|
+
isSelected ? 'bg-[var(--acc-l)]' : 'hover:bg-muted'
|
|
234
|
+
}`}
|
|
235
|
+
onClick={() => toggleRow(tag)}
|
|
236
|
+
>
|
|
237
|
+
<td className="px-4 py-[11px]">
|
|
238
|
+
<button
|
|
239
|
+
type="button"
|
|
240
|
+
onClick={(e) => {
|
|
241
|
+
e.stopPropagation()
|
|
242
|
+
toggleRow(tag)
|
|
243
|
+
}}
|
|
244
|
+
aria-expanded={isSelected}
|
|
245
|
+
className="text-foreground hover:text-primary text-left font-medium"
|
|
246
|
+
>
|
|
247
|
+
{tag.name}
|
|
248
|
+
</button>
|
|
249
|
+
</td>
|
|
250
|
+
<td className="px-4 py-[11px]">
|
|
251
|
+
<span
|
|
252
|
+
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${PLACEMENT_COLORS[tag.placement] ?? 'bg-muted text-foreground'}`}
|
|
253
|
+
>
|
|
254
|
+
{PLACEMENT_LABELS[tag.placement] ?? tag.placement}
|
|
255
|
+
</span>
|
|
256
|
+
</td>
|
|
257
|
+
<td className="text-muted-foreground px-4 py-[11px] text-center font-mono">
|
|
258
|
+
{tag.priority}
|
|
259
|
+
</td>
|
|
260
|
+
<td className="text-muted-foreground px-4 py-[11px]">{scopeLabel(tag)}</td>
|
|
261
|
+
<td className="px-4 py-[11px]">
|
|
262
|
+
{tag.consentCategory && tag.consentCategory !== 'none' ? (
|
|
263
|
+
<span className="rounded-[5px] border border-[var(--bdr)] bg-[var(--bg)] px-1.5 py-0.5 text-[11px] text-[var(--sub)]">
|
|
264
|
+
{CONSENT_LABELS[tag.consentCategory] ?? tag.consentCategory}
|
|
265
|
+
</span>
|
|
266
|
+
) : (
|
|
267
|
+
<span className="text-muted-foreground text-xs">—</span>
|
|
268
|
+
)}
|
|
269
|
+
</td>
|
|
270
|
+
<td
|
|
271
|
+
className="px-4 py-[11px] text-center"
|
|
272
|
+
onClick={(e) => e.stopPropagation()}
|
|
273
|
+
>
|
|
274
|
+
<EnabledSwitch
|
|
275
|
+
checked={tag.enabled}
|
|
276
|
+
disabled={togglingId === tag.id}
|
|
277
|
+
onCheckedChange={(next) => void toggleEnabled(tag, next)}
|
|
278
|
+
aria-label={`${tag.enabled ? 'Disable' : 'Enable'} ${tag.name}`}
|
|
279
|
+
/>
|
|
280
|
+
</td>
|
|
281
|
+
<td className="text-muted-foreground px-4 py-[11px] text-xs">
|
|
282
|
+
{formatUpdated(tag.updatedAt)}
|
|
283
|
+
</td>
|
|
284
|
+
</tr>
|
|
285
|
+
)
|
|
286
|
+
})}
|
|
287
|
+
</tbody>
|
|
288
|
+
</table>
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
</SplitPaneList>
|
|
292
|
+
|
|
293
|
+
<InspectorPane open={paneOpen} width={420} aria-label="Script editor">
|
|
294
|
+
<ScriptTagEditorPane
|
|
295
|
+
key={selection?.mode === 'edit' ? selection.id : 'new'}
|
|
296
|
+
tag={selectedTag}
|
|
297
|
+
onClose={() => setSelection(null)}
|
|
298
|
+
onChanged={handlePaneChanged}
|
|
299
|
+
/>
|
|
300
|
+
</InspectorPane>
|
|
301
|
+
</SplitPaneLayout>
|
|
227
302
|
</div>
|
|
228
303
|
)
|
|
229
304
|
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dashboard onboarding / getting-started block (design: Dashboard.html
|
|
5
|
+
* `OnboardingSection`). Shown only while the site is effectively empty —
|
|
6
|
+
* it REPLACES the Recent Activity / Publishing Queue / Content Health grid
|
|
7
|
+
* (see `shouldShowOnboarding`), sitting between the stats row and the
|
|
8
|
+
* Delivery API card.
|
|
9
|
+
*
|
|
10
|
+
* Steps are DERIVED from data the dashboard already fetches — the
|
|
11
|
+
* prototype's click-to-toggle checklist was mock behavior. No new API
|
|
12
|
+
* endpoints; every signal below comes from `/stats` or
|
|
13
|
+
* `/stats/api-delivery`:
|
|
14
|
+
*
|
|
15
|
+
* 1. Create your workspace — always complete (they're logged in).
|
|
16
|
+
* 2. Create your first Post/Page — post/page collection counts > 0.
|
|
17
|
+
* 3. Upload media assets — `totalMedia > 0`.
|
|
18
|
+
* 4. Configure SEO defaults — `avgSeoScore > 0`. The average SEO
|
|
19
|
+
* score is the only SEO signal in already-fetched data; it is 0 until
|
|
20
|
+
* content is scored, so the step reads incomplete on an empty site
|
|
21
|
+
* (honest, if conservative).
|
|
22
|
+
* 5. Get your API key — no API-key count exists in dashboard
|
|
23
|
+
* data, so we use recorded delivery-API traffic (`hasData` from
|
|
24
|
+
* /stats/api-delivery) as the proxy: traffic means the API is being
|
|
25
|
+
* consumed. Incomplete by default.
|
|
26
|
+
* 6. Invite team members — `totalUsers > 1`.
|
|
27
|
+
*
|
|
28
|
+
* Quick links deviate from the prototype in one place: "Webhooks guide"
|
|
29
|
+
* has no in-admin destination or docs URL (webhooks UI only exists inside
|
|
30
|
+
* individual form settings), so it is replaced with an internal SEO
|
|
31
|
+
* defaults link — no dead links.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { Check, ChevronRight, Code, Key, Search, Users, type LucideIcon } from 'lucide-react'
|
|
35
|
+
|
|
36
|
+
export interface OnboardingSignals {
|
|
37
|
+
/** Combined document count across post/page-type collections. */
|
|
38
|
+
postsAndPagesCount: number
|
|
39
|
+
/** `totalMedia` from /stats. */
|
|
40
|
+
mediaCount: number
|
|
41
|
+
/** `avgSeoScore` from /stats (0 when nothing is scored yet). */
|
|
42
|
+
seoScore: number
|
|
43
|
+
/** `totalUsers` from /stats. */
|
|
44
|
+
userCount: number
|
|
45
|
+
/** `hasData` from /stats/api-delivery — any delivery traffic recorded. */
|
|
46
|
+
hasApiTraffic: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface OnboardingStep {
|
|
50
|
+
id: string
|
|
51
|
+
label: string
|
|
52
|
+
done: boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function deriveOnboardingSteps(signals: OnboardingSignals): OnboardingStep[] {
|
|
56
|
+
return [
|
|
57
|
+
{ id: 'workspace', label: 'Create your workspace', done: true },
|
|
58
|
+
{
|
|
59
|
+
id: 'first-content',
|
|
60
|
+
label: 'Create your first Post or Page',
|
|
61
|
+
done: signals.postsAndPagesCount > 0,
|
|
62
|
+
},
|
|
63
|
+
{ id: 'media', label: 'Upload media assets', done: signals.mediaCount > 0 },
|
|
64
|
+
{ id: 'seo', label: 'Configure SEO defaults', done: signals.seoScore > 0 },
|
|
65
|
+
{ id: 'api-key', label: 'Get your API key', done: signals.hasApiTraffic },
|
|
66
|
+
{ id: 'invite', label: 'Invite team members', done: signals.userCount > 1 },
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The block appears only when content is essentially empty: zero posts +
|
|
72
|
+
* pages AND zero media (steps 2 and 3 both incomplete).
|
|
73
|
+
*/
|
|
74
|
+
export function shouldShowOnboarding(
|
|
75
|
+
signals: Pick<OnboardingSignals, 'postsAndPagesCount' | 'mediaCount'>,
|
|
76
|
+
): boolean {
|
|
77
|
+
return signals.postsAndPagesCount === 0 && signals.mediaCount === 0
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface QuickLink {
|
|
81
|
+
icon: LucideIcon
|
|
82
|
+
label: string
|
|
83
|
+
sub: string
|
|
84
|
+
path: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const QUICK_LINKS: QuickLink[] = [
|
|
88
|
+
{
|
|
89
|
+
icon: Code,
|
|
90
|
+
label: 'API Reference',
|
|
91
|
+
sub: 'REST & GraphQL docs',
|
|
92
|
+
path: '/settings?tab=api-keys',
|
|
93
|
+
},
|
|
94
|
+
{ icon: Search, label: 'SEO defaults', sub: 'Set up site metadata', path: '/settings?tab=seo' },
|
|
95
|
+
{ icon: Users, label: 'Invite team', sub: 'Add editors & admins', path: '/settings?tab=users' },
|
|
96
|
+
{ icon: Key, label: 'API Keys', sub: 'Manage access tokens', path: '/settings?tab=api-keys' },
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
export interface OnboardingBlockProps {
|
|
100
|
+
signals: OnboardingSignals
|
|
101
|
+
onNavigate?: (path: string) => void
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function OnboardingBlock({ signals, onNavigate }: OnboardingBlockProps) {
|
|
105
|
+
const steps = deriveOnboardingSteps(signals)
|
|
106
|
+
const done = steps.filter((s) => s.done).length
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className="space-y-3.5">
|
|
110
|
+
{/* Welcome banner ─────────────────────────────────────────────── */}
|
|
111
|
+
<section
|
|
112
|
+
aria-labelledby="ob-welcome-title"
|
|
113
|
+
className="rounded-[var(--r)] border border-[var(--bdr)] bg-[linear-gradient(135deg,var(--acc-l)_0%,var(--card)_60%)] px-7 py-6 shadow-[var(--shd)]"
|
|
114
|
+
>
|
|
115
|
+
<h2
|
|
116
|
+
id="ob-welcome-title"
|
|
117
|
+
className="text-[17px] font-medium tracking-[-0.3px] text-[var(--txt)]"
|
|
118
|
+
>
|
|
119
|
+
Welcome to ActuateCMS <span aria-hidden>👋</span>
|
|
120
|
+
</h2>
|
|
121
|
+
<p className="mt-1 max-w-[480px] text-[length:var(--fs-body)] text-[var(--sub)]">
|
|
122
|
+
Your headless CMS is ready. Complete these steps to start delivering content to your
|
|
123
|
+
frontend. {done}/{steps.length} steps complete.
|
|
124
|
+
</p>
|
|
125
|
+
<div
|
|
126
|
+
role="progressbar"
|
|
127
|
+
aria-label="Onboarding progress"
|
|
128
|
+
aria-valuemin={0}
|
|
129
|
+
aria-valuemax={steps.length}
|
|
130
|
+
aria-valuenow={done}
|
|
131
|
+
aria-valuetext={`${done} of ${steps.length} steps complete`}
|
|
132
|
+
className="mt-3 h-[6px] max-w-[400px] overflow-hidden rounded-full bg-[var(--bdr)]"
|
|
133
|
+
>
|
|
134
|
+
<div
|
|
135
|
+
className="h-full rounded-full bg-[var(--acc)] motion-safe:transition-[width] motion-safe:duration-[var(--motion-base)]"
|
|
136
|
+
style={{ width: `${(done / steps.length) * 100}%` }}
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
</section>
|
|
140
|
+
|
|
141
|
+
{/* Two-card grid ──────────────────────────────────────────────── */}
|
|
142
|
+
<div className="grid grid-cols-1 gap-3.5 lg:grid-cols-2">
|
|
143
|
+
{/* Getting started checklist */}
|
|
144
|
+
<section
|
|
145
|
+
aria-labelledby="ob-checklist-title"
|
|
146
|
+
className="rounded-[var(--r)] border border-[var(--bdr)] bg-[var(--card)] p-6 shadow-[var(--shd)]"
|
|
147
|
+
>
|
|
148
|
+
<h3
|
|
149
|
+
id="ob-checklist-title"
|
|
150
|
+
className="text-[length:var(--fs-card)] font-medium text-[var(--txt)]"
|
|
151
|
+
>
|
|
152
|
+
Getting started
|
|
153
|
+
</h3>
|
|
154
|
+
<p className="mt-1 mb-4 text-[length:var(--fs-sub)] leading-[1.55] text-[var(--sub)]">
|
|
155
|
+
Complete each step to get the most out of ActuateCMS.
|
|
156
|
+
</p>
|
|
157
|
+
<ul role="list" className="space-y-0.5">
|
|
158
|
+
{steps.map((step) => (
|
|
159
|
+
<li key={step.id} className="flex items-center gap-[11px] rounded-lg px-2.5 py-2">
|
|
160
|
+
<span
|
|
161
|
+
aria-hidden
|
|
162
|
+
className={`flex h-[19px] w-[19px] shrink-0 items-center justify-center rounded-full motion-safe:transition-colors motion-safe:duration-[var(--motion-base)] ${
|
|
163
|
+
step.done ? 'bg-[var(--suc)]' : 'border-2 border-[var(--bdr)]'
|
|
164
|
+
}`}
|
|
165
|
+
>
|
|
166
|
+
{step.done && (
|
|
167
|
+
<Check
|
|
168
|
+
size={11}
|
|
169
|
+
strokeWidth={2.5}
|
|
170
|
+
className="text-[var(--success-foreground)]"
|
|
171
|
+
/>
|
|
172
|
+
)}
|
|
173
|
+
</span>
|
|
174
|
+
<span
|
|
175
|
+
className={`text-[length:var(--fs-body)] ${
|
|
176
|
+
step.done ? 'text-[var(--muted)] line-through' : 'text-[var(--txt)]'
|
|
177
|
+
}`}
|
|
178
|
+
>
|
|
179
|
+
{step.label}
|
|
180
|
+
</span>
|
|
181
|
+
{/* Completion state must not rely on color/strikethrough alone. */}
|
|
182
|
+
<span className="sr-only">{step.done ? '(completed)' : '(not completed)'}</span>
|
|
183
|
+
</li>
|
|
184
|
+
))}
|
|
185
|
+
</ul>
|
|
186
|
+
</section>
|
|
187
|
+
|
|
188
|
+
{/* Quick links */}
|
|
189
|
+
<section
|
|
190
|
+
aria-labelledby="ob-links-title"
|
|
191
|
+
className="rounded-[var(--r)] border border-[var(--bdr)] bg-[var(--card)] p-6 shadow-[var(--shd)]"
|
|
192
|
+
>
|
|
193
|
+
<h3
|
|
194
|
+
id="ob-links-title"
|
|
195
|
+
className="text-[length:var(--fs-card)] font-medium text-[var(--txt)]"
|
|
196
|
+
>
|
|
197
|
+
Quick links
|
|
198
|
+
</h3>
|
|
199
|
+
<p className="mt-1 mb-4 text-[length:var(--fs-sub)] leading-[1.55] text-[var(--sub)]">
|
|
200
|
+
Helpful resources for building with ActuateCMS.
|
|
201
|
+
</p>
|
|
202
|
+
<ul role="list" className="space-y-0.5">
|
|
203
|
+
{QUICK_LINKS.map((link) => (
|
|
204
|
+
<li key={link.label}>
|
|
205
|
+
<button
|
|
206
|
+
type="button"
|
|
207
|
+
onClick={() => onNavigate?.(link.path)}
|
|
208
|
+
className="focus-visible:ring-ring flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left transition-colors hover:bg-[var(--acc-l)] focus:outline-none focus-visible:ring-2"
|
|
209
|
+
>
|
|
210
|
+
<span
|
|
211
|
+
aria-hidden
|
|
212
|
+
className="flex h-[30px] w-[30px] shrink-0 items-center justify-center rounded-[7px] bg-[var(--acc-l)] text-[var(--acc)]"
|
|
213
|
+
>
|
|
214
|
+
<link.icon size={14} />
|
|
215
|
+
</span>
|
|
216
|
+
<span className="min-w-0 flex-1">
|
|
217
|
+
<span className="block truncate text-[length:var(--fs-body)] font-medium text-[var(--txt)]">
|
|
218
|
+
{link.label}
|
|
219
|
+
</span>
|
|
220
|
+
<span className="block truncate text-[length:var(--fs-hint)] text-[var(--muted)]">
|
|
221
|
+
{link.sub}
|
|
222
|
+
</span>
|
|
223
|
+
</span>
|
|
224
|
+
<ChevronRight size={14} className="shrink-0 text-[var(--muted)]" aria-hidden />
|
|
225
|
+
</button>
|
|
226
|
+
</li>
|
|
227
|
+
))}
|
|
228
|
+
</ul>
|
|
229
|
+
</section>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
@@ -48,6 +48,12 @@ import { resolveDefaultSitePreview } from '../../lib/editor-preview.js'
|
|
|
48
48
|
interface PageSectionEditorProps {
|
|
49
49
|
/** undefined when creating a new page (`/pages/new`) */
|
|
50
50
|
documentId?: string
|
|
51
|
+
/**
|
|
52
|
+
* Parent page to preselect on a new page — set when arriving via
|
|
53
|
+
* "Add sub-page" in the Pages tree view (`/pages/new?parent=<id>`).
|
|
54
|
+
* Ignored when `documentId` is present.
|
|
55
|
+
*/
|
|
56
|
+
initialParentId?: string | null
|
|
51
57
|
config?: any
|
|
52
58
|
session?: any
|
|
53
59
|
onNavigate: (path: string) => void
|
|
@@ -73,6 +79,7 @@ function deriveCan(session: any): { canEdit: boolean; canPublish: boolean } {
|
|
|
73
79
|
|
|
74
80
|
export function PageSectionEditor({
|
|
75
81
|
documentId,
|
|
82
|
+
initialParentId,
|
|
76
83
|
config,
|
|
77
84
|
session,
|
|
78
85
|
onNavigate,
|
|
@@ -136,7 +143,7 @@ export function PageSectionEditor({
|
|
|
136
143
|
}
|
|
137
144
|
|
|
138
145
|
if (!documentId) {
|
|
139
|
-
setPage(emptyPage())
|
|
146
|
+
setPage(emptyPage(initialParentId ?? null))
|
|
140
147
|
setLoading(false)
|
|
141
148
|
return
|
|
142
149
|
}
|
|
@@ -160,7 +167,7 @@ export function PageSectionEditor({
|
|
|
160
167
|
return () => {
|
|
161
168
|
cancelled = true
|
|
162
169
|
}
|
|
163
|
-
}, [documentId, schemaError])
|
|
170
|
+
}, [documentId, initialParentId, schemaError])
|
|
164
171
|
|
|
165
172
|
// ─── Unsaved-changes guard (browser unload) ──────────────────────────
|
|
166
173
|
useEffect(() => {
|