@actuate-media/cms-admin 0.19.0 → 0.21.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 +13 -9
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/layout/sidebar-posts.test.js +6 -4
- package/dist/__tests__/layout/sidebar-posts.test.js.map +1 -1
- package/dist/__tests__/lib/seo-service-errors.test.d.ts +2 -0
- package/dist/__tests__/lib/seo-service-errors.test.d.ts.map +1 -0
- package/dist/__tests__/lib/seo-service-errors.test.js +46 -0
- package/dist/__tests__/lib/seo-service-errors.test.js.map +1 -0
- package/dist/__tests__/views/use-seo-resource.test.d.ts +2 -0
- package/dist/__tests__/views/use-seo-resource.test.d.ts.map +1 -0
- package/dist/__tests__/views/use-seo-resource.test.js +56 -0
- package/dist/__tests__/views/use-seo-resource.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/Breadcrumbs.d.ts.map +1 -1
- package/dist/components/Breadcrumbs.js +4 -1
- package/dist/components/Breadcrumbs.js.map +1 -1
- package/dist/components/seo/Drawer.d.ts +11 -0
- package/dist/components/seo/Drawer.d.ts.map +1 -0
- package/dist/components/seo/Drawer.js +17 -0
- package/dist/components/seo/Drawer.js.map +1 -0
- package/dist/components/seo/RedirectEditorDrawer.d.ts +9 -0
- package/dist/components/seo/RedirectEditorDrawer.d.ts.map +1 -0
- package/dist/components/seo/RedirectEditorDrawer.js +94 -0
- package/dist/components/seo/RedirectEditorDrawer.js.map +1 -0
- package/dist/components/seo/SeoCopilotDrawer.d.ts +10 -0
- package/dist/components/seo/SeoCopilotDrawer.d.ts.map +1 -0
- package/dist/components/seo/SeoCopilotDrawer.js +34 -0
- package/dist/components/seo/SeoCopilotDrawer.js.map +1 -0
- package/dist/components/seo/SeoEditorDrawer.d.ts +8 -0
- package/dist/components/seo/SeoEditorDrawer.d.ts.map +1 -0
- package/dist/components/seo/SeoEditorDrawer.js +120 -0
- package/dist/components/seo/SeoEditorDrawer.js.map +1 -0
- package/dist/components/seo/SeoIssueFixDrawer.d.ts +8 -0
- package/dist/components/seo/SeoIssueFixDrawer.d.ts.map +1 -0
- package/dist/components/seo/SeoIssueFixDrawer.js +111 -0
- package/dist/components/seo/SeoIssueFixDrawer.js.map +1 -0
- package/dist/components/seo/primitives.d.ts +53 -0
- package/dist/components/seo/primitives.d.ts.map +1 -0
- package/dist/components/seo/primitives.js +101 -0
- package/dist/components/seo/primitives.js.map +1 -0
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +8 -5
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/lib/seo-service.d.ts +322 -0
- package/dist/lib/seo-service.d.ts.map +1 -0
- package/dist/lib/seo-service.js +331 -0
- package/dist/lib/seo-service.js.map +1 -0
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +4 -1
- package/dist/router/index.js.map +1 -1
- package/dist/views/ApiKeys.d.ts +4 -1
- package/dist/views/ApiKeys.d.ts.map +1 -1
- package/dist/views/ApiKeys.js +5 -3
- package/dist/views/ApiKeys.js.map +1 -1
- package/dist/views/Dashboard.js +2 -2
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +36 -25
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/Posts/NewPostTypeInstructions.js +2 -2
- package/dist/views/Posts/post-type-presets.d.ts +5 -0
- package/dist/views/Posts/post-type-presets.d.ts.map +1 -1
- package/dist/views/Posts/post-type-presets.js +10 -64
- package/dist/views/Posts/post-type-presets.js.map +1 -1
- package/dist/views/SEO.d.ts.map +1 -1
- package/dist/views/SEO.js +89 -132
- package/dist/views/SEO.js.map +1 -1
- package/dist/views/Settings.d.ts +4 -1
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +45 -6
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/Users.d.ts +4 -1
- package/dist/views/Users.d.ts.map +1 -1
- package/dist/views/Users.js +5 -3
- package/dist/views/Users.js.map +1 -1
- package/dist/views/seo/AuditTab.d.ts +6 -0
- package/dist/views/seo/AuditTab.d.ts.map +1 -0
- package/dist/views/seo/AuditTab.js +57 -0
- package/dist/views/seo/AuditTab.js.map +1 -0
- package/dist/views/seo/ContentTab.d.ts +5 -0
- package/dist/views/seo/ContentTab.d.ts.map +1 -0
- package/dist/views/seo/ContentTab.js +27 -0
- package/dist/views/seo/ContentTab.js.map +1 -0
- package/dist/views/seo/OverviewTab.d.ts +8 -0
- package/dist/views/seo/OverviewTab.d.ts.map +1 -0
- package/dist/views/seo/OverviewTab.js +35 -0
- package/dist/views/seo/OverviewTab.js.map +1 -0
- package/dist/views/seo/RedirectsTab.d.ts +2 -0
- package/dist/views/seo/RedirectsTab.d.ts.map +1 -0
- package/dist/views/seo/RedirectsTab.js +130 -0
- package/dist/views/seo/RedirectsTab.js.map +1 -0
- package/dist/views/seo/TechnicalTab.d.ts +2 -0
- package/dist/views/seo/TechnicalTab.d.ts.map +1 -0
- package/dist/views/seo/TechnicalTab.js +105 -0
- package/dist/views/seo/TechnicalTab.js.map +1 -0
- package/dist/views/seo/useSeoResource.d.ts +8 -0
- package/dist/views/seo/useSeoResource.d.ts.map +1 -0
- package/dist/views/seo/useSeoResource.js +55 -0
- package/dist/views/seo/useSeoResource.js.map +1 -0
- package/package.json +2 -2
- package/src/AdminRoot.tsx +13 -9
- package/src/__tests__/layout/sidebar-posts.test.ts +6 -4
- package/src/__tests__/lib/seo-service-errors.test.ts +60 -0
- package/src/__tests__/views/use-seo-resource.test.tsx +68 -0
- package/src/components/Breadcrumbs.tsx +4 -1
- package/src/components/seo/Drawer.tsx +65 -0
- package/src/components/seo/RedirectEditorDrawer.tsx +235 -0
- package/src/components/seo/SeoCopilotDrawer.tsx +76 -0
- package/src/components/seo/SeoEditorDrawer.tsx +359 -0
- package/src/components/seo/SeoIssueFixDrawer.tsx +244 -0
- package/src/components/seo/primitives.tsx +240 -0
- package/src/layout/Sidebar.tsx +7 -10
- package/src/lib/seo-service.ts +660 -0
- package/src/router/index.ts +4 -1
- package/src/views/ApiKeys.tsx +10 -5
- package/src/views/Dashboard.tsx +2 -2
- package/src/views/MediaBrowser.tsx +89 -67
- package/src/views/Posts/NewPostTypeInstructions.tsx +2 -2
- package/src/views/Posts/post-type-presets.ts +15 -64
- package/src/views/SEO.tsx +186 -863
- package/src/views/Settings.tsx +93 -40
- package/src/views/Users.tsx +10 -4
- package/src/views/seo/AuditTab.tsx +229 -0
- package/src/views/seo/ContentTab.tsx +164 -0
- package/src/views/seo/OverviewTab.tsx +257 -0
- package/src/views/seo/RedirectsTab.tsx +398 -0
- package/src/views/seo/TechnicalTab.tsx +315 -0
- package/src/views/seo/useSeoResource.ts +66 -0
package/src/views/ApiKeys.tsx
CHANGED
|
@@ -19,6 +19,9 @@ import { cmsApi } from '../lib/api.js'
|
|
|
19
19
|
|
|
20
20
|
export interface ApiKeysProps {
|
|
21
21
|
onNavigate?: (path: string) => void
|
|
22
|
+
/** When rendered inside the Settings tab shell, drop the page padding and
|
|
23
|
+
* demote the heading so it reads as a section rather than a second page. */
|
|
24
|
+
embedded?: boolean
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
interface ApiKeyRecord {
|
|
@@ -68,12 +71,14 @@ function formatDate(value: string | null): string {
|
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
export function ApiKeys(
|
|
74
|
+
export function ApiKeys({ embedded = false }: ApiKeysProps = {}) {
|
|
72
75
|
const { data, loading, error, refetch } = useApiData<ApiKeyRecord[]>('/api-keys')
|
|
73
76
|
const [showCreate, setShowCreate] = useState(false)
|
|
74
77
|
const [createdKey, setCreatedKey] = useState<{ key: string; record: ApiKeyRecord } | null>(null)
|
|
75
78
|
|
|
76
79
|
const keys = data ?? []
|
|
80
|
+
const rootPadding = embedded ? '' : 'p-4 sm:p-6'
|
|
81
|
+
const Heading = embedded ? 'h2' : 'h1'
|
|
77
82
|
|
|
78
83
|
const handleRevoke = async (id: string, name: string) => {
|
|
79
84
|
if (!confirm(`Revoke API key "${name}"? This cannot be undone.`)) return
|
|
@@ -93,20 +98,20 @@ export function ApiKeys(_props: ApiKeysProps) {
|
|
|
93
98
|
|
|
94
99
|
if (loading) {
|
|
95
100
|
return (
|
|
96
|
-
<div className=
|
|
101
|
+
<div className={`flex h-64 items-center justify-center ${embedded ? '' : 'p-4 sm:p-6'}`}>
|
|
97
102
|
<Loader2 className="h-6 w-6 animate-spin text-blue-600" />
|
|
98
103
|
</div>
|
|
99
104
|
)
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
return (
|
|
103
|
-
<div className=
|
|
108
|
+
<div className={rootPadding}>
|
|
104
109
|
<div className="mb-6 flex flex-col justify-between gap-3 sm:flex-row sm:items-center">
|
|
105
110
|
<div>
|
|
106
|
-
<
|
|
111
|
+
<Heading className="flex items-center gap-2 text-xl font-semibold text-gray-900 sm:text-2xl">
|
|
107
112
|
<KeyRound className="h-5 w-5 text-gray-500" />
|
|
108
113
|
API Keys
|
|
109
|
-
</
|
|
114
|
+
</Heading>
|
|
110
115
|
<p className="mt-1 text-sm text-gray-500">
|
|
111
116
|
Long-lived credentials for programmatic access. Use the{' '}
|
|
112
117
|
<code className="rounded bg-gray-100 px-1 py-0.5 text-xs">Authorization: Bearer</code>{' '}
|
package/src/views/Dashboard.tsx
CHANGED
|
@@ -425,7 +425,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
425
425
|
{ label: 'New Page', icon: Plus, onClick: () => nav(`/${pagesSlug}/new`) },
|
|
426
426
|
{ label: 'Upload Media', icon: Upload, onClick: () => nav('/media') },
|
|
427
427
|
{ label: 'New Form', icon: Plus, onClick: () => nav('/forms') },
|
|
428
|
-
{ label: 'View API', icon: Globe, onClick: () => nav('/api-keys') },
|
|
428
|
+
{ label: 'View API', icon: Globe, onClick: () => nav('/settings?tab=api-keys') },
|
|
429
429
|
]
|
|
430
430
|
return items
|
|
431
431
|
}, [collections, onNavigate]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
@@ -899,7 +899,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
899
899
|
</div>
|
|
900
900
|
<button
|
|
901
901
|
className="text-brand inline-flex items-center gap-1 text-xs font-medium hover:underline"
|
|
902
|
-
onClick={() => nav('/api-keys')}
|
|
902
|
+
onClick={() => nav('/settings?tab=api-keys')}
|
|
903
903
|
>
|
|
904
904
|
API docs <ExternalLink className="h-3 w-3" />
|
|
905
905
|
</button>
|
|
@@ -30,7 +30,6 @@ import { sortByRelevance, type SortConfig, toggleSort } from '../lib/search.js'
|
|
|
30
30
|
import { useApiData } from '../lib/useApiData.js'
|
|
31
31
|
import { cmsApi } from '../lib/api.js'
|
|
32
32
|
import { FolderTree, type FolderSelection } from '../components/FolderTree.js'
|
|
33
|
-
import { FocalPointPicker } from '../components/FocalPointPicker.js'
|
|
34
33
|
|
|
35
34
|
interface MediaItem {
|
|
36
35
|
id: number | string
|
|
@@ -120,8 +119,6 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
120
119
|
const [editAlt, setEditAlt] = useState('')
|
|
121
120
|
const [editTitle, setEditTitle] = useState('')
|
|
122
121
|
const [editFilename, setEditFilename] = useState('')
|
|
123
|
-
const [focalX, setFocalX] = useState(0.5)
|
|
124
|
-
const [focalY, setFocalY] = useState(0.5)
|
|
125
122
|
const [saving, setSaving] = useState(false)
|
|
126
123
|
const [aiGenerating, setAiGenerating] = useState<string | null>(null)
|
|
127
124
|
const [uploading, setUploading] = useState(false)
|
|
@@ -157,8 +154,6 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
157
154
|
setEditAlt(item.altTag ?? '')
|
|
158
155
|
setEditTitle(item.title ?? '')
|
|
159
156
|
setEditFilename(item.name)
|
|
160
|
-
setFocalX((item as any).focalPointX ?? 0.5)
|
|
161
|
-
setFocalY((item as any).focalPointY ?? 0.5)
|
|
162
157
|
}
|
|
163
158
|
|
|
164
159
|
const closeDetail = () => {
|
|
@@ -187,8 +182,6 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
187
182
|
alt: editAlt,
|
|
188
183
|
title: editTitle,
|
|
189
184
|
filename: editFilename,
|
|
190
|
-
focalX,
|
|
191
|
-
focalY,
|
|
192
185
|
}),
|
|
193
186
|
})
|
|
194
187
|
setSaving(false)
|
|
@@ -244,18 +237,38 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
244
237
|
return
|
|
245
238
|
}
|
|
246
239
|
|
|
247
|
-
|
|
240
|
+
if (!activeItem) {
|
|
241
|
+
setAiGenerating(null)
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const res = await cmsApi<{ alt?: string; title?: string; alternatives?: string[] }>(
|
|
246
|
+
'/ai/media-metadata',
|
|
247
|
+
{ method: 'POST', body: JSON.stringify({ mediaId: activeItem.id }) },
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if (res.error) {
|
|
251
|
+
// AI plugin/provider not configured (e.g. local dev): fall back to a
|
|
252
|
+
// basic filename-derived suggestion so the action still helps.
|
|
253
|
+
const base = (activeItem.name ?? '')
|
|
254
|
+
.replace(/\.\w+$/, '')
|
|
255
|
+
.replace(/[-_]/g, ' ')
|
|
256
|
+
.trim()
|
|
257
|
+
if (field === 'alt') {
|
|
258
|
+
setEditAlt(base ? `Image of ${base}` : '')
|
|
259
|
+
} else {
|
|
260
|
+
setEditTitle(base.replace(/\b\w/g, (c) => c.toUpperCase()))
|
|
261
|
+
}
|
|
262
|
+
toast.info('AI unavailable — generated a basic suggestion from the filename')
|
|
263
|
+
setAiGenerating(null)
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
248
267
|
if (field === 'alt') {
|
|
249
|
-
|
|
250
|
-
setEditAlt(generated)
|
|
268
|
+
setEditAlt(res.data?.alt ?? '')
|
|
251
269
|
toast.success('Alt tag generated by AI')
|
|
252
|
-
} else
|
|
253
|
-
|
|
254
|
-
activeItem?.name
|
|
255
|
-
.replace(/[-_]/g, ' ')
|
|
256
|
-
.replace(/\.\w+$/, '')
|
|
257
|
-
.replace(/\b\w/g, (c) => c.toUpperCase()) ?? ''
|
|
258
|
-
setEditTitle(generated)
|
|
270
|
+
} else {
|
|
271
|
+
setEditTitle(res.data?.title ?? '')
|
|
259
272
|
toast.success('Title generated by AI')
|
|
260
273
|
}
|
|
261
274
|
setAiGenerating(null)
|
|
@@ -533,7 +546,7 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
533
546
|
>
|
|
534
547
|
{viewMode === 'grid' ? (
|
|
535
548
|
<div
|
|
536
|
-
className={`grid h-full gap-
|
|
549
|
+
className={`grid h-full auto-rows-min content-start gap-3 overflow-y-auto p-2 sm:gap-4 sm:p-3 ${
|
|
537
550
|
panelOpen
|
|
538
551
|
? 'grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'
|
|
539
552
|
: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-8'
|
|
@@ -542,10 +555,17 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
542
555
|
{filteredAndSorted.map((item) => {
|
|
543
556
|
const isActive = activeItem?.id === item.id
|
|
544
557
|
const hasIssues = !item.altTag || !item.title || item.usedOn?.length === 0
|
|
558
|
+
const typeLabel = (
|
|
559
|
+
item.name.includes('.')
|
|
560
|
+
? item.name.split('.').pop()!
|
|
561
|
+
: item.format || item.type || ''
|
|
562
|
+
)
|
|
563
|
+
.replace(/^.*\//, '')
|
|
564
|
+
.toUpperCase()
|
|
545
565
|
return (
|
|
546
566
|
<div
|
|
547
567
|
key={item.id}
|
|
548
|
-
className={`group relative
|
|
568
|
+
className={`group relative flex cursor-pointer flex-col overflow-hidden rounded-lg border-2 transition-all ${
|
|
549
569
|
isActive
|
|
550
570
|
? 'border-blue-500 ring-2 ring-blue-200'
|
|
551
571
|
: selectedMedia.includes(item.id)
|
|
@@ -556,45 +576,61 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
556
576
|
draggable
|
|
557
577
|
onDragStart={(e) => handleDragStart(e, item.id)}
|
|
558
578
|
>
|
|
559
|
-
<div className="
|
|
560
|
-
<
|
|
561
|
-
|
|
562
|
-
<div className="absolute inset-0 bg-linear-to-t from-black/60 to-transparent opacity-0 transition-opacity group-hover:opacity-100">
|
|
563
|
-
<div className="absolute right-0 bottom-0 left-0 p-2">
|
|
564
|
-
<p className="truncate text-xs font-medium text-white">{item.name}</p>
|
|
565
|
-
<p className="text-xs text-white/80">{item.size}</p>
|
|
579
|
+
<div className="relative aspect-square w-full overflow-hidden">
|
|
580
|
+
<div className="flex h-full w-full items-center justify-center bg-gray-100">
|
|
581
|
+
<MediaPreview item={item} />
|
|
566
582
|
</div>
|
|
567
|
-
|
|
568
|
-
|
|
583
|
+
{hasIssues && (
|
|
584
|
+
<div
|
|
585
|
+
className="absolute top-1.5 left-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-yellow-500"
|
|
586
|
+
title="Needs attention"
|
|
587
|
+
>
|
|
588
|
+
<AlertTriangle className="h-3 w-3 text-white" />
|
|
589
|
+
</div>
|
|
590
|
+
)}
|
|
569
591
|
<div
|
|
570
|
-
className="absolute top-1.5
|
|
571
|
-
|
|
592
|
+
className="absolute top-1.5 right-1.5 opacity-0 transition-opacity group-hover:opacity-100"
|
|
593
|
+
onClick={(e) => handleCheckbox(e, item.id)}
|
|
572
594
|
>
|
|
573
|
-
<
|
|
595
|
+
<div
|
|
596
|
+
className={`flex h-5 w-5 items-center justify-center rounded border-2 transition-colors ${
|
|
597
|
+
selectedMedia.includes(item.id)
|
|
598
|
+
? 'border-blue-600 bg-blue-600'
|
|
599
|
+
: 'border-gray-400 bg-white/80'
|
|
600
|
+
}`}
|
|
601
|
+
>
|
|
602
|
+
{selectedMedia.includes(item.id) && (
|
|
603
|
+
<svg
|
|
604
|
+
className="h-3 w-3 text-white"
|
|
605
|
+
fill="none"
|
|
606
|
+
viewBox="0 0 12 12"
|
|
607
|
+
>
|
|
608
|
+
<path
|
|
609
|
+
d="M10 3L4.5 8.5L2 6"
|
|
610
|
+
stroke="currentColor"
|
|
611
|
+
strokeWidth="2"
|
|
612
|
+
strokeLinecap="round"
|
|
613
|
+
strokeLinejoin="round"
|
|
614
|
+
/>
|
|
615
|
+
</svg>
|
|
616
|
+
)}
|
|
617
|
+
</div>
|
|
574
618
|
</div>
|
|
575
|
-
|
|
576
|
-
<div
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
<div
|
|
581
|
-
className={`flex h-5 w-5 items-center justify-center rounded border-2 transition-colors ${
|
|
582
|
-
selectedMedia.includes(item.id)
|
|
583
|
-
? 'border-blue-600 bg-blue-600'
|
|
584
|
-
: 'border-gray-400 bg-white/80'
|
|
619
|
+
</div>
|
|
620
|
+
<div className="flex items-center gap-2 border-t border-gray-200 bg-white px-2 py-1.5">
|
|
621
|
+
<span
|
|
622
|
+
className={`mt-0.5 h-2 w-2 shrink-0 self-start rounded-full ${
|
|
623
|
+
hasIssues ? 'bg-yellow-500' : 'bg-green-500'
|
|
585
624
|
}`}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
/>
|
|
596
|
-
</svg>
|
|
597
|
-
)}
|
|
625
|
+
title={hasIssues ? 'Needs attention' : 'SEO complete'}
|
|
626
|
+
/>
|
|
627
|
+
<div className="min-w-0 flex-1">
|
|
628
|
+
<p className="truncate text-xs font-medium text-gray-900">
|
|
629
|
+
{item.name}
|
|
630
|
+
</p>
|
|
631
|
+
<p className="truncate text-xs text-gray-500">
|
|
632
|
+
{[typeLabel, item.size].filter(Boolean).join(' · ')}
|
|
633
|
+
</p>
|
|
598
634
|
</div>
|
|
599
635
|
</div>
|
|
600
636
|
</div>
|
|
@@ -872,20 +908,6 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
872
908
|
</div>
|
|
873
909
|
</div>
|
|
874
910
|
|
|
875
|
-
{isImageMedia(activeItem) && activeItem.url && (
|
|
876
|
-
<div className="border-b border-gray-200 p-4">
|
|
877
|
-
<FocalPointPicker
|
|
878
|
-
imageUrl={activeItem.url}
|
|
879
|
-
focalX={focalX}
|
|
880
|
-
focalY={focalY}
|
|
881
|
-
onChange={(x, y) => {
|
|
882
|
-
setFocalX(x)
|
|
883
|
-
setFocalY(y)
|
|
884
|
-
}}
|
|
885
|
-
/>
|
|
886
|
-
</div>
|
|
887
|
-
)}
|
|
888
|
-
|
|
889
911
|
<div className="border-b border-gray-200 p-4">
|
|
890
912
|
<h4 className="mb-3 text-xs font-semibold tracking-wide text-gray-500 uppercase">
|
|
891
913
|
Used On {activeItem.usedOn && `(${activeItem.usedOn.length})`}
|
|
@@ -11,8 +11,8 @@ import { POST_TYPE_PRESETS, type PresetColor, type QuickPreset } from './post-ty
|
|
|
11
11
|
* /posts/types/new — Quick Post Type gallery + snippet generator.
|
|
12
12
|
*
|
|
13
13
|
* Two ways to start:
|
|
14
|
-
* 1. Pick a Quick Post Type preset (
|
|
15
|
-
*
|
|
14
|
+
* 1. Pick a Quick Post Type preset (Blog) — pre-fills the form and
|
|
15
|
+
* tailored extra fields.
|
|
16
16
|
* 2. Fill the form from scratch.
|
|
17
17
|
*
|
|
18
18
|
* Either way the page emits a paste-ready `actuate.config.ts` snippet.
|
|
@@ -10,6 +10,11 @@ import type { PostTypeIconName } from '../../components/posts/icons.js'
|
|
|
10
10
|
* Presets are NOT a runtime data model — they only seed the snippet.
|
|
11
11
|
* Collections stay code-defined; this just saves the developer from
|
|
12
12
|
* hand-authoring common field sets.
|
|
13
|
+
*
|
|
14
|
+
* Only the canonical **Blog** post type ships as a built-in preset. Other
|
|
15
|
+
* content types (case studies, portfolio, team, …) are available as opt-in
|
|
16
|
+
* collection factories in `@actuate-media/cms-core` (`collections/presets.ts`)
|
|
17
|
+
* rather than shipped here as defaults.
|
|
13
18
|
*/
|
|
14
19
|
|
|
15
20
|
export type PresetColor = 'purple' | 'green' | 'orange' | 'blue' | 'red' | 'pink'
|
|
@@ -34,72 +39,18 @@ export interface QuickPreset {
|
|
|
34
39
|
|
|
35
40
|
export const POST_TYPE_PRESETS: QuickPreset[] = [
|
|
36
41
|
{
|
|
37
|
-
key: '
|
|
38
|
-
label: '
|
|
39
|
-
singular: '
|
|
40
|
-
plural: '
|
|
41
|
-
slug: '
|
|
42
|
-
description: '
|
|
43
|
-
color: 'green',
|
|
44
|
-
icon: 'briefcase',
|
|
45
|
-
extraFields: [
|
|
46
|
-
"client: { type: 'text', label: 'Client Name' },",
|
|
47
|
-
"industry: { type: 'text', label: 'Industry' },",
|
|
48
|
-
"results: { type: 'richText', label: 'Results & Outcomes' },",
|
|
49
|
-
],
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
key: 'news',
|
|
53
|
-
label: 'News',
|
|
54
|
-
singular: 'News Item',
|
|
55
|
-
plural: 'News',
|
|
56
|
-
slug: 'news',
|
|
57
|
-
description: 'Company announcements, press releases, and industry news.',
|
|
58
|
-
color: 'orange',
|
|
59
|
-
icon: 'newspaper',
|
|
60
|
-
extraFields: ["source: { type: 'text', label: 'Source' },"],
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
key: 'portfolio',
|
|
64
|
-
label: 'Portfolio',
|
|
65
|
-
singular: 'Portfolio Item',
|
|
66
|
-
plural: 'Portfolio',
|
|
67
|
-
slug: 'portfolio',
|
|
68
|
-
description: 'Showcase projects, work samples, and creative pieces.',
|
|
69
|
-
color: 'blue',
|
|
70
|
-
icon: 'image',
|
|
71
|
-
extraFields: [
|
|
72
|
-
"projectUrl: { type: 'url', label: 'Project URL' },",
|
|
73
|
-
"gallery: { type: 'media', label: 'Gallery' },",
|
|
74
|
-
],
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
key: 'events',
|
|
78
|
-
label: 'Events',
|
|
79
|
-
singular: 'Event',
|
|
80
|
-
plural: 'Events',
|
|
81
|
-
slug: 'events',
|
|
82
|
-
description: 'Webinars, conferences, and scheduled happenings.',
|
|
83
|
-
color: 'pink',
|
|
84
|
-
icon: 'calendar',
|
|
85
|
-
extraFields: [
|
|
86
|
-
"startDate: { type: 'date', label: 'Start Date', includeTime: true },",
|
|
87
|
-
"endDate: { type: 'date', label: 'End Date', includeTime: true },",
|
|
88
|
-
"location: { type: 'text', label: 'Location' },",
|
|
89
|
-
],
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
key: 'team',
|
|
93
|
-
label: 'Team Members',
|
|
94
|
-
singular: 'Team Member',
|
|
95
|
-
plural: 'Team',
|
|
96
|
-
slug: 'team',
|
|
97
|
-
description: 'Staff bios, headshots, and role information.',
|
|
42
|
+
key: 'blog',
|
|
43
|
+
label: 'Blog',
|
|
44
|
+
singular: 'Post',
|
|
45
|
+
plural: 'Posts',
|
|
46
|
+
slug: 'blog',
|
|
47
|
+
description: 'Long-form articles and blog posts with author, category, and tags.',
|
|
98
48
|
color: 'purple',
|
|
99
|
-
icon: '
|
|
49
|
+
icon: 'newspaper',
|
|
100
50
|
extraFields: [
|
|
101
|
-
"
|
|
102
|
-
"
|
|
51
|
+
"author: { type: 'text', label: 'Author' },",
|
|
52
|
+
"category: { type: 'text', label: 'Category' },",
|
|
53
|
+
"tags: { type: 'array', label: 'Tags', fields: { value: { type: 'text', label: 'Tag' } } },",
|
|
103
54
|
],
|
|
104
55
|
},
|
|
105
56
|
]
|