@actuate-media/cms-admin 0.15.0 → 0.16.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 +148 -2
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/components/posts-badges.test.d.ts +2 -0
- package/dist/__tests__/components/posts-badges.test.d.ts.map +1 -0
- package/dist/__tests__/components/posts-badges.test.js +68 -0
- package/dist/__tests__/components/posts-badges.test.js.map +1 -0
- package/dist/__tests__/layout/sidebar-posts.test.d.ts +2 -0
- package/dist/__tests__/layout/sidebar-posts.test.d.ts.map +1 -0
- package/dist/__tests__/layout/sidebar-posts.test.js +162 -0
- package/dist/__tests__/layout/sidebar-posts.test.js.map +1 -0
- package/dist/__tests__/lib/page-editor-service.test.d.ts +2 -0
- package/dist/__tests__/lib/page-editor-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/page-editor-service.test.js +137 -0
- package/dist/__tests__/lib/page-editor-service.test.js.map +1 -0
- package/dist/__tests__/lib/pages-service.test.d.ts +2 -0
- package/dist/__tests__/lib/pages-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/pages-service.test.js +140 -0
- package/dist/__tests__/lib/pages-service.test.js.map +1 -0
- package/dist/__tests__/lib/post-editor-service.test.d.ts +2 -0
- package/dist/__tests__/lib/post-editor-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/post-editor-service.test.js +185 -0
- package/dist/__tests__/lib/post-editor-service.test.js.map +1 -0
- package/dist/__tests__/lib/posts-service.test.d.ts +2 -0
- package/dist/__tests__/lib/posts-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/posts-service.test.js +244 -0
- package/dist/__tests__/lib/posts-service.test.js.map +1 -0
- package/dist/__tests__/views/posts-list-view.test.d.ts +2 -0
- package/dist/__tests__/views/posts-list-view.test.d.ts.map +1 -0
- package/dist/__tests__/views/posts-list-view.test.js +98 -0
- package/dist/__tests__/views/posts-list-view.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/pages/tag-badges.d.ts +21 -0
- package/dist/components/pages/tag-badges.d.ts.map +1 -0
- package/dist/components/pages/tag-badges.js +56 -0
- package/dist/components/pages/tag-badges.js.map +1 -0
- package/dist/components/posts/badges.d.ts +25 -0
- package/dist/components/posts/badges.d.ts.map +1 -0
- package/dist/components/posts/badges.js +123 -0
- package/dist/components/posts/badges.js.map +1 -0
- package/dist/components/posts/icons.d.ts +12 -0
- package/dist/components/posts/icons.d.ts.map +1 -0
- package/dist/components/posts/icons.js +26 -0
- package/dist/components/posts/icons.js.map +1 -0
- package/dist/layout/Layout.js +36 -8
- package/dist/layout/Layout.js.map +1 -1
- package/dist/layout/Sidebar.d.ts +9 -0
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +100 -48
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/lib/page-editor-service.d.ts +45 -0
- package/dist/lib/page-editor-service.d.ts.map +1 -0
- package/dist/lib/page-editor-service.js +153 -0
- package/dist/lib/page-editor-service.js.map +1 -0
- package/dist/lib/pages-service.d.ts +143 -0
- package/dist/lib/pages-service.d.ts.map +1 -0
- package/dist/lib/pages-service.js +444 -0
- package/dist/lib/pages-service.js.map +1 -0
- package/dist/lib/post-editor-service.d.ts +57 -0
- package/dist/lib/post-editor-service.d.ts.map +1 -0
- package/dist/lib/post-editor-service.js +234 -0
- package/dist/lib/post-editor-service.js.map +1 -0
- package/dist/lib/posts-service.d.ts +73 -0
- package/dist/lib/posts-service.d.ts.map +1 -0
- package/dist/lib/posts-service.js +245 -0
- package/dist/lib/posts-service.js.map +1 -0
- package/dist/lib/preview-link.d.ts +32 -0
- package/dist/lib/preview-link.d.ts.map +1 -0
- package/dist/lib/preview-link.js +40 -0
- package/dist/lib/preview-link.js.map +1 -0
- package/dist/router/index.d.ts +1 -0
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +9 -1
- package/dist/router/index.js.map +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +14 -3
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/Pages/PageTagManager.d.ts +14 -0
- package/dist/views/Pages/PageTagManager.d.ts.map +1 -0
- package/dist/views/Pages/PageTagManager.js +145 -0
- package/dist/views/Pages/PageTagManager.js.map +1 -0
- package/dist/views/Pages/PagesListView.d.ts +18 -0
- package/dist/views/Pages/PagesListView.d.ts.map +1 -0
- package/dist/views/Pages/PagesListView.js +287 -0
- package/dist/views/Pages/PagesListView.js.map +1 -0
- package/dist/views/Posts/NewPostPicker.d.ts +16 -0
- package/dist/views/Posts/NewPostPicker.d.ts.map +1 -0
- package/dist/views/Posts/NewPostPicker.js +59 -0
- package/dist/views/Posts/NewPostPicker.js.map +1 -0
- package/dist/views/Posts/NewPostTypeInstructions.d.ts +18 -0
- package/dist/views/Posts/NewPostTypeInstructions.d.ts.map +1 -0
- package/dist/views/Posts/NewPostTypeInstructions.js +147 -0
- package/dist/views/Posts/NewPostTypeInstructions.js.map +1 -0
- package/dist/views/Posts/PostTypesView.d.ts +26 -0
- package/dist/views/Posts/PostTypesView.d.ts.map +1 -0
- package/dist/views/Posts/PostTypesView.js +52 -0
- package/dist/views/Posts/PostTypesView.js.map +1 -0
- package/dist/views/Posts/PostsListView.d.ts +25 -0
- package/dist/views/Posts/PostsListView.d.ts.map +1 -0
- package/dist/views/Posts/PostsListView.js +239 -0
- package/dist/views/Posts/PostsListView.js.map +1 -0
- package/dist/views/Posts/post-type-presets.d.ts +32 -0
- package/dist/views/Posts/post-type-presets.d.ts.map +1 -0
- package/dist/views/Posts/post-type-presets.js +72 -0
- package/dist/views/Posts/post-type-presets.js.map +1 -0
- package/dist/views/page-builder/Widgets.d.ts +13 -0
- package/dist/views/page-builder/Widgets.d.ts.map +1 -0
- package/dist/views/page-builder/Widgets.js +15 -0
- package/dist/views/page-builder/Widgets.js.map +1 -0
- package/dist/views/page-editor/AddSectionModal.d.ts +20 -0
- package/dist/views/page-editor/AddSectionModal.d.ts.map +1 -0
- package/dist/views/page-editor/AddSectionModal.js +22 -0
- package/dist/views/page-editor/AddSectionModal.js.map +1 -0
- package/dist/views/page-editor/EditorCanvas.d.ts +18 -0
- package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -0
- package/dist/views/page-editor/EditorCanvas.js +64 -0
- package/dist/views/page-editor/EditorCanvas.js.map +1 -0
- package/dist/views/page-editor/EditorTopBar.d.ts +30 -0
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -0
- package/dist/views/page-editor/EditorTopBar.js +42 -0
- package/dist/views/page-editor/EditorTopBar.js.map +1 -0
- package/dist/views/page-editor/PagePreview.d.ts +13 -0
- package/dist/views/page-editor/PagePreview.d.ts.map +1 -0
- package/dist/views/page-editor/PagePreview.js +37 -0
- package/dist/views/page-editor/PagePreview.js.map +1 -0
- package/dist/views/page-editor/PageSectionEditor.d.ts +16 -0
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -0
- package/dist/views/page-editor/PageSectionEditor.js +272 -0
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -0
- package/dist/views/page-editor/PageSettingsModal.d.ts +12 -0
- package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -0
- package/dist/views/page-editor/PageSettingsModal.js +52 -0
- package/dist/views/page-editor/PageSettingsModal.js.map +1 -0
- package/dist/views/page-editor/SectionInspector.d.ts +17 -0
- package/dist/views/page-editor/SectionInspector.d.ts.map +1 -0
- package/dist/views/page-editor/SectionInspector.js +98 -0
- package/dist/views/page-editor/SectionInspector.js.map +1 -0
- package/dist/views/page-editor/SectionsPanel.d.ts +15 -0
- package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -0
- package/dist/views/page-editor/SectionsPanel.js +43 -0
- package/dist/views/page-editor/SectionsPanel.js.map +1 -0
- package/dist/views/page-editor/ValidationSummary.d.ts +9 -0
- package/dist/views/page-editor/ValidationSummary.d.ts.map +1 -0
- package/dist/views/page-editor/ValidationSummary.js +10 -0
- package/dist/views/page-editor/ValidationSummary.js.map +1 -0
- package/dist/views/page-editor/section-icons.d.ts +4 -0
- package/dist/views/page-editor/section-icons.d.ts.map +1 -0
- package/dist/views/page-editor/section-icons.js +20 -0
- package/dist/views/page-editor/section-icons.js.map +1 -0
- package/dist/views/page-editor/section-types.d.ts +11 -0
- package/dist/views/page-editor/section-types.d.ts.map +1 -0
- package/dist/views/page-editor/section-types.js +10 -0
- package/dist/views/page-editor/section-types.js.map +1 -0
- package/dist/views/page-editor/sections/ArticleBodySection.d.ts +13 -0
- package/dist/views/page-editor/sections/ArticleBodySection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/ArticleBodySection.js +23 -0
- package/dist/views/page-editor/sections/ArticleBodySection.js.map +1 -0
- package/dist/views/page-editor/sections/AuthorBioSection.d.ts +8 -0
- package/dist/views/page-editor/sections/AuthorBioSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/AuthorBioSection.js +21 -0
- package/dist/views/page-editor/sections/AuthorBioSection.js.map +1 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts +5 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.js +10 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.js.map +1 -0
- package/dist/views/page-editor/sections/CtaBannerSection.d.ts +5 -0
- package/dist/views/page-editor/sections/CtaBannerSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/CtaBannerSection.js +10 -0
- package/dist/views/page-editor/sections/CtaBannerSection.js.map +1 -0
- package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts +5 -0
- package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/FeatureHighlightSection.js +18 -0
- package/dist/views/page-editor/sections/FeatureHighlightSection.js.map +1 -0
- package/dist/views/page-editor/sections/GallerySection.d.ts +6 -0
- package/dist/views/page-editor/sections/GallerySection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/GallerySection.js +17 -0
- package/dist/views/page-editor/sections/GallerySection.js.map +1 -0
- package/dist/views/page-editor/sections/HeroBannerSection.d.ts +5 -0
- package/dist/views/page-editor/sections/HeroBannerSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/HeroBannerSection.js +11 -0
- package/dist/views/page-editor/sections/HeroBannerSection.js.map +1 -0
- package/dist/views/page-editor/sections/MissionSection.d.ts +6 -0
- package/dist/views/page-editor/sections/MissionSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/MissionSection.js +12 -0
- package/dist/views/page-editor/sections/MissionSection.js.map +1 -0
- package/dist/views/page-editor/sections/PullQuoteSection.d.ts +6 -0
- package/dist/views/page-editor/sections/PullQuoteSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/PullQuoteSection.js +13 -0
- package/dist/views/page-editor/sections/PullQuoteSection.js.map +1 -0
- package/dist/views/page-editor/sections/RelatedPostsSection.d.ts +12 -0
- package/dist/views/page-editor/sections/RelatedPostsSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/RelatedPostsSection.js +23 -0
- package/dist/views/page-editor/sections/RelatedPostsSection.js.map +1 -0
- package/dist/views/page-editor/sections/SectionRenderer.d.ts +22 -0
- package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -0
- package/dist/views/page-editor/sections/SectionRenderer.js +52 -0
- package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -0
- package/dist/views/page-editor/sections/index.d.ts +14 -0
- package/dist/views/page-editor/sections/index.d.ts.map +1 -0
- package/dist/views/page-editor/sections/index.js +12 -0
- package/dist/views/page-editor/sections/index.js.map +1 -0
- package/dist/views/page-editor/sections/parts.d.ts +55 -0
- package/dist/views/page-editor/sections/parts.d.ts.map +1 -0
- package/dist/views/page-editor/sections/parts.js +60 -0
- package/dist/views/page-editor/sections/parts.js.map +1 -0
- package/dist/views/page-editor/viewports.d.ts +14 -0
- package/dist/views/page-editor/viewports.d.ts.map +1 -0
- package/dist/views/page-editor/viewports.js +12 -0
- package/dist/views/page-editor/viewports.js.map +1 -0
- package/dist/views/post-editor/PostEditorCanvas.d.ts +21 -0
- package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -0
- package/dist/views/post-editor/PostEditorCanvas.js +58 -0
- package/dist/views/post-editor/PostEditorCanvas.js.map +1 -0
- package/dist/views/post-editor/PostFieldsModal.d.ts +12 -0
- package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -0
- package/dist/views/post-editor/PostFieldsModal.js +53 -0
- package/dist/views/post-editor/PostFieldsModal.js.map +1 -0
- package/dist/views/post-editor/PostHeader.d.ts +15 -0
- package/dist/views/post-editor/PostHeader.d.ts.map +1 -0
- package/dist/views/post-editor/PostHeader.js +27 -0
- package/dist/views/post-editor/PostHeader.js.map +1 -0
- package/dist/views/post-editor/PostHeaderPanel.d.ts +11 -0
- package/dist/views/post-editor/PostHeaderPanel.d.ts.map +1 -0
- package/dist/views/post-editor/PostHeaderPanel.js +21 -0
- package/dist/views/post-editor/PostHeaderPanel.js.map +1 -0
- package/dist/views/post-editor/PostPreview.d.ts +14 -0
- package/dist/views/post-editor/PostPreview.d.ts.map +1 -0
- package/dist/views/post-editor/PostPreview.js +53 -0
- package/dist/views/post-editor/PostPreview.js.map +1 -0
- package/dist/views/post-editor/PostSectionEditor.d.ts +17 -0
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -0
- package/dist/views/post-editor/PostSectionEditor.js +290 -0
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -0
- package/dist/views/post-editor/PostTemplateEditor.d.ts +9 -0
- package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -0
- package/dist/views/post-editor/PostTemplateEditor.js +144 -0
- package/dist/views/post-editor/PostTemplateEditor.js.map +1 -0
- package/package.json +2 -2
- package/src/AdminRoot.tsx +213 -2
- package/src/__tests__/components/posts-badges.test.tsx +77 -0
- package/src/__tests__/layout/sidebar-posts.test.ts +179 -0
- package/src/__tests__/lib/page-editor-service.test.ts +173 -0
- package/src/__tests__/lib/pages-service.test.ts +157 -0
- package/src/__tests__/lib/post-editor-service.test.ts +215 -0
- package/src/__tests__/lib/posts-service.test.ts +279 -0
- package/src/__tests__/views/posts-list-view.test.ts +110 -0
- package/src/components/pages/tag-badges.tsx +104 -0
- package/src/components/posts/badges.tsx +170 -0
- package/src/components/posts/icons.ts +40 -0
- package/src/layout/Layout.tsx +66 -10
- package/src/layout/Sidebar.tsx +116 -50
- package/src/lib/page-editor-service.ts +233 -0
- package/src/lib/pages-service.ts +599 -0
- package/src/lib/post-editor-service.ts +342 -0
- package/src/lib/posts-service.ts +366 -0
- package/src/lib/preview-link.ts +63 -0
- package/src/router/index.ts +10 -1
- package/src/views/Dashboard.tsx +15 -3
- package/src/views/Pages/PageTagManager.tsx +436 -0
- package/src/views/Pages/PagesListView.tsx +944 -0
- package/src/views/Posts/NewPostPicker.tsx +147 -0
- package/src/views/Posts/NewPostTypeInstructions.tsx +344 -0
- package/src/views/Posts/PostTypesView.tsx +194 -0
- package/src/views/Posts/PostsListView.tsx +869 -0
- package/src/views/Posts/post-type-presets.ts +105 -0
- package/src/views/page-builder/Widgets.tsx +44 -0
- package/src/views/page-editor/AddSectionModal.tsx +56 -0
- package/src/views/page-editor/EditorCanvas.tsx +127 -0
- package/src/views/page-editor/EditorTopBar.tsx +233 -0
- package/src/views/page-editor/PagePreview.tsx +96 -0
- package/src/views/page-editor/PageSectionEditor.tsx +476 -0
- package/src/views/page-editor/PageSettingsModal.tsx +154 -0
- package/src/views/page-editor/SectionInspector.tsx +599 -0
- package/src/views/page-editor/SectionsPanel.tsx +270 -0
- package/src/views/page-editor/ValidationSummary.tsx +53 -0
- package/src/views/page-editor/section-icons.ts +33 -0
- package/src/views/page-editor/section-types.ts +27 -0
- package/src/views/page-editor/sections/ArticleBodySection.tsx +57 -0
- package/src/views/page-editor/sections/AuthorBioSection.tsx +61 -0
- package/src/views/page-editor/sections/ByTheNumbersSection.tsx +41 -0
- package/src/views/page-editor/sections/CtaBannerSection.tsx +29 -0
- package/src/views/page-editor/sections/FeatureHighlightSection.tsx +63 -0
- package/src/views/page-editor/sections/GallerySection.tsx +54 -0
- package/src/views/page-editor/sections/HeroBannerSection.tsx +44 -0
- package/src/views/page-editor/sections/MissionSection.tsx +22 -0
- package/src/views/page-editor/sections/PullQuoteSection.tsx +27 -0
- package/src/views/page-editor/sections/RelatedPostsSection.tsx +55 -0
- package/src/views/page-editor/sections/SectionRenderer.tsx +102 -0
- package/src/views/page-editor/sections/index.ts +13 -0
- package/src/views/page-editor/sections/parts.tsx +113 -0
- package/src/views/page-editor/viewports.ts +25 -0
- package/src/views/post-editor/PostEditorCanvas.tsx +123 -0
- package/src/views/post-editor/PostFieldsModal.tsx +196 -0
- package/src/views/post-editor/PostHeader.tsx +87 -0
- package/src/views/post-editor/PostHeaderPanel.tsx +107 -0
- package/src/views/post-editor/PostPreview.tsx +114 -0
- package/src/views/post-editor/PostSectionEditor.tsx +506 -0
- package/src/views/post-editor/PostTemplateEditor.tsx +347 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { ArrowRight, ArrowLeft } from 'lucide-react'
|
|
5
|
+
import { fetchPostTypes, type PostType } from '../../lib/posts-service.js'
|
|
6
|
+
import { paletteFor } from '../../components/posts/badges.js'
|
|
7
|
+
import { resolvePostTypeIcon } from '../../components/posts/icons.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Type-picker landing page for "+ New Post".
|
|
11
|
+
*
|
|
12
|
+
* When zero or one post type is configured we'd ordinarily auto-route
|
|
13
|
+
* to the per-collection editor, but the spec calls out always showing
|
|
14
|
+
* a chooser when "+ New Post" is clicked. We keep that behavior so
|
|
15
|
+
* the experience is consistent regardless of how many types exist.
|
|
16
|
+
*
|
|
17
|
+
* Picking a card navigates to the section-based Post editor's new-post
|
|
18
|
+
* route (`/posts/<slug>/new`), which seeds the post from that type's
|
|
19
|
+
* template so the author starts from the type's default layout.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export function NewPostPicker({ onNavigate }: { onNavigate: (path: string) => void }) {
|
|
23
|
+
const [types, setTypes] = useState<PostType[]>([])
|
|
24
|
+
const [loading, setLoading] = useState(true)
|
|
25
|
+
const [error, setError] = useState<string | null>(null)
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
let cancelled = false
|
|
29
|
+
fetchPostTypes()
|
|
30
|
+
.then((res) => {
|
|
31
|
+
if (cancelled) return
|
|
32
|
+
// With a single Post Type there's nothing to choose — forward
|
|
33
|
+
// straight to its editor so the picker doesn't flash a
|
|
34
|
+
// one-card screen.
|
|
35
|
+
if (res.length === 1) {
|
|
36
|
+
onNavigate(`/posts/${res[0]!.slug}/new`)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
setTypes(res)
|
|
40
|
+
setLoading(false)
|
|
41
|
+
})
|
|
42
|
+
.catch((err: unknown) => {
|
|
43
|
+
if (cancelled) return
|
|
44
|
+
setError(err instanceof Error ? err.message : 'Failed to load Post Types')
|
|
45
|
+
setLoading(false)
|
|
46
|
+
})
|
|
47
|
+
return () => {
|
|
48
|
+
cancelled = true
|
|
49
|
+
}
|
|
50
|
+
}, [onNavigate])
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="mx-auto flex h-full max-w-3xl flex-col gap-6 p-4 sm:p-6">
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
onClick={() => onNavigate('/posts')}
|
|
57
|
+
className="text-muted-foreground hover:text-foreground inline-flex w-fit items-center gap-1.5 text-sm transition-colors"
|
|
58
|
+
>
|
|
59
|
+
<ArrowLeft className="h-3.5 w-3.5" aria-hidden />
|
|
60
|
+
Back to Posts
|
|
61
|
+
</button>
|
|
62
|
+
|
|
63
|
+
<div className="flex flex-col gap-1">
|
|
64
|
+
<h1 className="text-foreground text-xl font-medium tracking-tight sm:text-2xl">
|
|
65
|
+
Choose a Post Type
|
|
66
|
+
</h1>
|
|
67
|
+
<p className="text-muted-foreground text-sm">
|
|
68
|
+
Pick which content model you want to use for this post. Each type has its own fields, URL
|
|
69
|
+
base, and SEO defaults.
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{loading && <Skeleton />}
|
|
74
|
+
{!loading && error && (
|
|
75
|
+
<div className="border-border bg-card rounded-xl border p-8 text-center">
|
|
76
|
+
<div className="text-foreground mb-2 text-sm font-medium">{error}</div>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
{!loading && !error && types.length === 0 && (
|
|
80
|
+
<div className="border-border bg-card rounded-xl border p-8 text-center">
|
|
81
|
+
<div className="text-foreground mb-2 text-sm font-medium">No Post Types configured.</div>
|
|
82
|
+
<p className="text-muted-foreground mb-4 text-sm">
|
|
83
|
+
Add a collection with <code className="font-mono">type: 'post'</code> to your CMS config
|
|
84
|
+
to start creating posts.
|
|
85
|
+
</p>
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
onClick={() => onNavigate('/posts/types/new')}
|
|
89
|
+
className="text-primary text-sm hover:underline"
|
|
90
|
+
>
|
|
91
|
+
View setup instructions →
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
{!loading && !error && types.length > 0 && (
|
|
96
|
+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
97
|
+
{types.map((t) => {
|
|
98
|
+
const palette = paletteFor(t.accentColor)
|
|
99
|
+
const Icon = resolvePostTypeIcon(t.icon)
|
|
100
|
+
return (
|
|
101
|
+
<button
|
|
102
|
+
key={t.slug}
|
|
103
|
+
type="button"
|
|
104
|
+
onClick={() => onNavigate(`/posts/${t.slug}/new`)}
|
|
105
|
+
className="border-border bg-card focus-visible:ring-ring flex items-center gap-3 rounded-xl border p-4 text-left shadow-sm transition-shadow hover:shadow-md focus:outline-none focus-visible:ring-2"
|
|
106
|
+
>
|
|
107
|
+
<span
|
|
108
|
+
className={`inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg ${palette.bg} ${palette.text}`}
|
|
109
|
+
>
|
|
110
|
+
<Icon className="h-5 w-5" aria-hidden />
|
|
111
|
+
</span>
|
|
112
|
+
<span className="min-w-0 flex-1">
|
|
113
|
+
<span className="text-foreground block text-sm font-medium">
|
|
114
|
+
{t.singularName}
|
|
115
|
+
</span>
|
|
116
|
+
<span className="text-muted-foreground block truncate text-xs">
|
|
117
|
+
{t.description ?? `New entry in ${t.name}`}
|
|
118
|
+
</span>
|
|
119
|
+
</span>
|
|
120
|
+
<ArrowRight className="text-muted-foreground h-4 w-4 shrink-0" aria-hidden />
|
|
121
|
+
</button>
|
|
122
|
+
)
|
|
123
|
+
})}
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function Skeleton() {
|
|
131
|
+
return (
|
|
132
|
+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
133
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
134
|
+
<div
|
|
135
|
+
key={i}
|
|
136
|
+
className="border-border bg-card flex items-center gap-3 rounded-xl border p-4"
|
|
137
|
+
>
|
|
138
|
+
<div className="bg-muted h-10 w-10 animate-pulse rounded-lg" />
|
|
139
|
+
<div className="flex-1">
|
|
140
|
+
<div className="bg-muted mb-2 h-3 w-1/3 animate-pulse rounded" />
|
|
141
|
+
<div className="bg-muted h-2.5 w-2/3 animate-pulse rounded" />
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from 'react'
|
|
4
|
+
import { ArrowLeft, Check, Copy, Sparkles } from 'lucide-react'
|
|
5
|
+
import { toast } from 'sonner'
|
|
6
|
+
import { resolvePostTypeIcon, type PostTypeIconName } from '../../components/posts/icons.js'
|
|
7
|
+
import { paletteFor } from '../../components/posts/badges.js'
|
|
8
|
+
import { POST_TYPE_PRESETS, type PresetColor, type QuickPreset } from './post-type-presets.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* /posts/types/new — Quick Post Type gallery + snippet generator.
|
|
12
|
+
*
|
|
13
|
+
* Two ways to start:
|
|
14
|
+
* 1. Pick a Quick Post Type preset (Case Studies, News, Portfolio,
|
|
15
|
+
* Events, Team) — pre-fills the form and tailored extra fields.
|
|
16
|
+
* 2. Fill the form from scratch.
|
|
17
|
+
*
|
|
18
|
+
* Either way the page emits a paste-ready `actuate.config.ts` snippet.
|
|
19
|
+
*
|
|
20
|
+
* Why this isn't a runtime form-driven CRUD:
|
|
21
|
+
* Collections in ActuateCMS are file-defined in `actuate.config.ts`.
|
|
22
|
+
* Writing to that file at runtime would require either a parallel
|
|
23
|
+
* DB-only post-type model that duplicates the Collection concept, or
|
|
24
|
+
* mutating a source file on disk — which breaks Vercel's read-only
|
|
25
|
+
* serverless filesystem and any immutable deploy model. So we
|
|
26
|
+
* generate the snippet and the developer commits + redeploys. Same
|
|
27
|
+
* model Sanity, Keystone, and Payload use for code-defined schemas.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const SLUG_RE = /^[a-z][a-z0-9-]*$/
|
|
31
|
+
|
|
32
|
+
const ICON_OPTIONS: { value: PostTypeIconName; label: string }[] = [
|
|
33
|
+
{ value: 'file-text', label: 'Document' },
|
|
34
|
+
{ value: 'briefcase', label: 'Briefcase' },
|
|
35
|
+
{ value: 'newspaper', label: 'Newspaper' },
|
|
36
|
+
{ value: 'book', label: 'Book' },
|
|
37
|
+
{ value: 'folder', label: 'Folder' },
|
|
38
|
+
{ value: 'layers', label: 'Layers' },
|
|
39
|
+
{ value: 'image', label: 'Image' },
|
|
40
|
+
{ value: 'calendar', label: 'Calendar' },
|
|
41
|
+
{ value: 'users', label: 'People' },
|
|
42
|
+
{ value: 'help', label: 'Help' },
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
interface DraftType {
|
|
46
|
+
slug: string
|
|
47
|
+
singular: string
|
|
48
|
+
plural: string
|
|
49
|
+
color: PresetColor
|
|
50
|
+
icon: PostTypeIconName
|
|
51
|
+
description: string
|
|
52
|
+
/** Extra field lines beyond the shared base, seeded by a preset. */
|
|
53
|
+
extraFields: string[]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const BLANK_DRAFT: DraftType = {
|
|
57
|
+
slug: '',
|
|
58
|
+
singular: '',
|
|
59
|
+
plural: '',
|
|
60
|
+
color: 'purple',
|
|
61
|
+
icon: 'file-text',
|
|
62
|
+
description: '',
|
|
63
|
+
extraFields: [],
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function NewPostTypeInstructions({ onNavigate }: { onNavigate: (p: string) => void }) {
|
|
67
|
+
const [draft, setDraft] = useState<DraftType>(BLANK_DRAFT)
|
|
68
|
+
const [activePreset, setActivePreset] = useState<string | null>(null)
|
|
69
|
+
const [copied, setCopied] = useState(false)
|
|
70
|
+
const snippet = useMemo(() => renderSnippet(draft), [draft])
|
|
71
|
+
|
|
72
|
+
const slugValid = SLUG_RE.test(draft.slug)
|
|
73
|
+
|
|
74
|
+
const applyPreset = (preset: QuickPreset) => {
|
|
75
|
+
setActivePreset(preset.key)
|
|
76
|
+
setDraft({
|
|
77
|
+
slug: preset.slug,
|
|
78
|
+
singular: preset.singular,
|
|
79
|
+
plural: preset.plural,
|
|
80
|
+
color: preset.color,
|
|
81
|
+
icon: preset.icon,
|
|
82
|
+
description: preset.description,
|
|
83
|
+
extraFields: preset.extraFields,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Manual edits clear the active-preset highlight but keep any
|
|
88
|
+
// preset-seeded extra fields (the user may still want them).
|
|
89
|
+
const patchDraft = (patch: Partial<DraftType>) => {
|
|
90
|
+
setActivePreset(null)
|
|
91
|
+
setDraft((d) => ({ ...d, ...patch }))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const copy = async () => {
|
|
95
|
+
try {
|
|
96
|
+
await navigator.clipboard.writeText(snippet)
|
|
97
|
+
setCopied(true)
|
|
98
|
+
toast.success('Snippet copied to clipboard')
|
|
99
|
+
setTimeout(() => setCopied(false), 1800)
|
|
100
|
+
} catch {
|
|
101
|
+
toast.error('Copy failed — select the snippet manually')
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div className="mx-auto flex h-full max-w-3xl flex-col gap-6 p-4 sm:p-6">
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
onClick={() => onNavigate('/posts/types')}
|
|
110
|
+
className="text-muted-foreground hover:text-foreground inline-flex w-fit items-center gap-1.5 text-sm transition-colors"
|
|
111
|
+
>
|
|
112
|
+
<ArrowLeft className="h-3.5 w-3.5" aria-hidden />
|
|
113
|
+
Back to Post Types
|
|
114
|
+
</button>
|
|
115
|
+
|
|
116
|
+
<div className="flex flex-col gap-1">
|
|
117
|
+
<h1 className="text-foreground text-xl font-medium tracking-tight sm:text-2xl">
|
|
118
|
+
Add a Post Type
|
|
119
|
+
</h1>
|
|
120
|
+
<p className="text-muted-foreground text-sm">
|
|
121
|
+
Post Types are content models defined in{' '}
|
|
122
|
+
<code className="font-mono">actuate.config.ts</code>. Start from a quick template or fill
|
|
123
|
+
in the details below, then paste the generated snippet into your config and redeploy.
|
|
124
|
+
</p>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{/* Quick Post Type gallery */}
|
|
128
|
+
<section aria-labelledby="quick-types-heading" className="flex flex-col gap-3">
|
|
129
|
+
<div className="flex items-center gap-1.5">
|
|
130
|
+
<Sparkles className="text-muted-foreground h-3.5 w-3.5" aria-hidden />
|
|
131
|
+
<h2 id="quick-types-heading" className="text-foreground text-sm font-medium">
|
|
132
|
+
Quick start
|
|
133
|
+
</h2>
|
|
134
|
+
</div>
|
|
135
|
+
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
|
|
136
|
+
{POST_TYPE_PRESETS.map((preset) => {
|
|
137
|
+
const palette = paletteFor(preset.color)
|
|
138
|
+
const Icon = resolvePostTypeIcon(preset.icon)
|
|
139
|
+
const selected = activePreset === preset.key
|
|
140
|
+
return (
|
|
141
|
+
<button
|
|
142
|
+
key={preset.key}
|
|
143
|
+
type="button"
|
|
144
|
+
onClick={() => applyPreset(preset)}
|
|
145
|
+
aria-pressed={selected}
|
|
146
|
+
className={`focus-visible:ring-ring flex flex-col items-start gap-2 rounded-xl border p-3 text-left shadow-sm transition-colors focus:outline-none focus-visible:ring-2 ${
|
|
147
|
+
selected
|
|
148
|
+
? 'border-primary bg-primary/5'
|
|
149
|
+
: 'border-border bg-card hover:border-primary/40 hover:bg-accent'
|
|
150
|
+
}`}
|
|
151
|
+
>
|
|
152
|
+
<span
|
|
153
|
+
className={`inline-flex h-8 w-8 items-center justify-center rounded-lg ${palette.bg} ${palette.text}`}
|
|
154
|
+
>
|
|
155
|
+
<Icon className="h-4 w-4" aria-hidden />
|
|
156
|
+
</span>
|
|
157
|
+
<span className="text-foreground text-sm leading-tight font-medium">
|
|
158
|
+
{preset.label}
|
|
159
|
+
</span>
|
|
160
|
+
</button>
|
|
161
|
+
)
|
|
162
|
+
})}
|
|
163
|
+
</div>
|
|
164
|
+
</section>
|
|
165
|
+
|
|
166
|
+
{/* Detail form */}
|
|
167
|
+
<div className="bg-card border-border grid grid-cols-1 gap-4 rounded-xl border p-5 shadow-sm sm:grid-cols-2">
|
|
168
|
+
<Field
|
|
169
|
+
label="Slug"
|
|
170
|
+
htmlFor="pt-slug"
|
|
171
|
+
error={draft.slug && !slugValid ? 'lowercase letters, numbers, and dashes only' : null}
|
|
172
|
+
>
|
|
173
|
+
<input
|
|
174
|
+
id="pt-slug"
|
|
175
|
+
value={draft.slug}
|
|
176
|
+
onChange={(e) => patchDraft({ slug: e.target.value.trim() })}
|
|
177
|
+
className="border-border bg-background text-foreground focus:border-primary focus:ring-primary/30 w-full rounded-lg border px-3 py-1.5 text-sm focus:ring-2 focus:outline-none"
|
|
178
|
+
placeholder="reviews"
|
|
179
|
+
/>
|
|
180
|
+
</Field>
|
|
181
|
+
<Field label="Singular label" htmlFor="pt-singular">
|
|
182
|
+
<input
|
|
183
|
+
id="pt-singular"
|
|
184
|
+
value={draft.singular}
|
|
185
|
+
onChange={(e) => patchDraft({ singular: e.target.value })}
|
|
186
|
+
className="border-border bg-background text-foreground focus:border-primary focus:ring-primary/30 w-full rounded-lg border px-3 py-1.5 text-sm focus:ring-2 focus:outline-none"
|
|
187
|
+
placeholder="Review"
|
|
188
|
+
/>
|
|
189
|
+
</Field>
|
|
190
|
+
<Field label="Plural label" htmlFor="pt-plural">
|
|
191
|
+
<input
|
|
192
|
+
id="pt-plural"
|
|
193
|
+
value={draft.plural}
|
|
194
|
+
onChange={(e) => patchDraft({ plural: e.target.value })}
|
|
195
|
+
className="border-border bg-background text-foreground focus:border-primary focus:ring-primary/30 w-full rounded-lg border px-3 py-1.5 text-sm focus:ring-2 focus:outline-none"
|
|
196
|
+
placeholder="Reviews"
|
|
197
|
+
/>
|
|
198
|
+
</Field>
|
|
199
|
+
<Field label="Description" htmlFor="pt-desc">
|
|
200
|
+
<input
|
|
201
|
+
id="pt-desc"
|
|
202
|
+
value={draft.description}
|
|
203
|
+
onChange={(e) => patchDraft({ description: e.target.value })}
|
|
204
|
+
className="border-border bg-background text-foreground focus:border-primary focus:ring-primary/30 w-full rounded-lg border px-3 py-1.5 text-sm focus:ring-2 focus:outline-none"
|
|
205
|
+
placeholder="Product reviews and editorial pieces."
|
|
206
|
+
/>
|
|
207
|
+
</Field>
|
|
208
|
+
<Field label="Accent color" htmlFor="pt-color">
|
|
209
|
+
<select
|
|
210
|
+
id="pt-color"
|
|
211
|
+
value={draft.color}
|
|
212
|
+
onChange={(e) => patchDraft({ color: e.target.value as PresetColor })}
|
|
213
|
+
className="border-border bg-background text-foreground focus:border-primary focus:ring-primary/30 w-full rounded-lg border px-3 py-1.5 text-sm focus:ring-2 focus:outline-none"
|
|
214
|
+
>
|
|
215
|
+
<option value="purple">Purple</option>
|
|
216
|
+
<option value="green">Green</option>
|
|
217
|
+
<option value="orange">Orange</option>
|
|
218
|
+
<option value="blue">Blue</option>
|
|
219
|
+
<option value="red">Red</option>
|
|
220
|
+
<option value="pink">Pink</option>
|
|
221
|
+
</select>
|
|
222
|
+
</Field>
|
|
223
|
+
<Field label="Icon" htmlFor="pt-icon">
|
|
224
|
+
<select
|
|
225
|
+
id="pt-icon"
|
|
226
|
+
value={draft.icon}
|
|
227
|
+
onChange={(e) => patchDraft({ icon: e.target.value as PostTypeIconName })}
|
|
228
|
+
className="border-border bg-background text-foreground focus:border-primary focus:ring-primary/30 w-full rounded-lg border px-3 py-1.5 text-sm focus:ring-2 focus:outline-none"
|
|
229
|
+
>
|
|
230
|
+
{ICON_OPTIONS.map((o) => (
|
|
231
|
+
<option key={o.value} value={o.value}>
|
|
232
|
+
{o.label}
|
|
233
|
+
</option>
|
|
234
|
+
))}
|
|
235
|
+
</select>
|
|
236
|
+
</Field>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<div>
|
|
240
|
+
<div className="mb-2 flex items-center justify-between">
|
|
241
|
+
<h2 className="text-foreground text-sm font-medium">
|
|
242
|
+
Add to <code className="font-mono">actuate.config.ts</code>
|
|
243
|
+
</h2>
|
|
244
|
+
<button
|
|
245
|
+
type="button"
|
|
246
|
+
onClick={copy}
|
|
247
|
+
disabled={!slugValid}
|
|
248
|
+
className="border-border bg-background text-foreground hover:bg-accent inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40"
|
|
249
|
+
>
|
|
250
|
+
{copied ? (
|
|
251
|
+
<Check className="h-3.5 w-3.5" aria-hidden />
|
|
252
|
+
) : (
|
|
253
|
+
<Copy className="h-3.5 w-3.5" aria-hidden />
|
|
254
|
+
)}
|
|
255
|
+
{copied ? 'Copied' : 'Copy snippet'}
|
|
256
|
+
</button>
|
|
257
|
+
</div>
|
|
258
|
+
<pre className="border-border bg-muted text-foreground overflow-x-auto rounded-lg border p-4 text-xs leading-relaxed">
|
|
259
|
+
<code>{snippet}</code>
|
|
260
|
+
</pre>
|
|
261
|
+
<p className="text-muted-foreground mt-3 text-xs">
|
|
262
|
+
After adding the snippet and redeploying, the new type will appear in the Posts area tabs
|
|
263
|
+
and on this Post Types page automatically.
|
|
264
|
+
</p>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function Field({
|
|
271
|
+
label,
|
|
272
|
+
htmlFor,
|
|
273
|
+
error,
|
|
274
|
+
children,
|
|
275
|
+
}: {
|
|
276
|
+
label: string
|
|
277
|
+
htmlFor: string
|
|
278
|
+
error?: string | null
|
|
279
|
+
children: React.ReactNode
|
|
280
|
+
}) {
|
|
281
|
+
return (
|
|
282
|
+
<div className="flex flex-col gap-1">
|
|
283
|
+
<label htmlFor={htmlFor} className="text-foreground text-sm font-medium">
|
|
284
|
+
{label}
|
|
285
|
+
</label>
|
|
286
|
+
{children}
|
|
287
|
+
{error ? <span className="text-destructive text-xs">{error}</span> : null}
|
|
288
|
+
</div>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function renderSnippet(draft: DraftType): string {
|
|
293
|
+
const safeSlug = draft.slug || 'new-type'
|
|
294
|
+
const extraFieldsBlock =
|
|
295
|
+
draft.extraFields.length > 0
|
|
296
|
+
? '\n // type-specific fields\n' +
|
|
297
|
+
draft.extraFields.map((line) => ` ${line}`).join('\n') +
|
|
298
|
+
'\n'
|
|
299
|
+
: ''
|
|
300
|
+
|
|
301
|
+
return `// Add this entry to the \`collections\` map in actuate.config.ts:
|
|
302
|
+
collections: {
|
|
303
|
+
// …existing collections,
|
|
304
|
+
'${safeSlug}': {
|
|
305
|
+
slug: '${safeSlug}',
|
|
306
|
+
labels: {
|
|
307
|
+
singular: '${escapeString(draft.singular || 'Entry')}',
|
|
308
|
+
plural: '${escapeString(draft.plural || 'Entries')}',
|
|
309
|
+
},
|
|
310
|
+
type: 'post',
|
|
311
|
+
urlPrefix: '${safeSlug}',
|
|
312
|
+
admin: {
|
|
313
|
+
group: 'Posts',
|
|
314
|
+
icon: '${draft.icon}',
|
|
315
|
+
color: '${draft.color}',
|
|
316
|
+
description: '${escapeString(draft.description)}',
|
|
317
|
+
},
|
|
318
|
+
fields: {
|
|
319
|
+
title: { type: 'text', label: 'Title', required: true },
|
|
320
|
+
slug: { type: 'slug', label: 'Slug', from: 'title' },
|
|
321
|
+
excerpt: { type: 'text', label: 'Excerpt' },
|
|
322
|
+
featuredImage: { type: 'media', label: 'Featured Image' },
|
|
323
|
+
body: { type: 'richText', label: 'Body' },
|
|
324
|
+
status: {
|
|
325
|
+
type: 'select',
|
|
326
|
+
label: 'Status',
|
|
327
|
+
options: [
|
|
328
|
+
{ label: 'Draft', value: 'draft' },
|
|
329
|
+
{ label: 'Published', value: 'published' },
|
|
330
|
+
{ label: 'Scheduled', value: 'scheduled' },
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
publishDate: { type: 'date', label: 'Publish Date', includeTime: true },
|
|
334
|
+
metaTitle: { type: 'text', label: 'SEO Title' },
|
|
335
|
+
metaDescription: { type: 'text', label: 'SEO Description' },
|
|
336
|
+
${extraFieldsBlock} },
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
`
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function escapeString(input: string): string {
|
|
343
|
+
return input.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
|
|
344
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { LayoutTemplate, Plus } from 'lucide-react'
|
|
5
|
+
import { fetchPosts, type PostType } from '../../lib/posts-service.js'
|
|
6
|
+
import { paletteFor } from '../../components/posts/badges.js'
|
|
7
|
+
import { resolvePostTypeIcon } from '../../components/posts/icons.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Post Types — card grid that mirrors the reference screenshot. Each
|
|
11
|
+
* card surfaces:
|
|
12
|
+
* - colored top border (accent)
|
|
13
|
+
* - icon badge in upper-left
|
|
14
|
+
* - title + description
|
|
15
|
+
* - footer row: post count · field count · URL base
|
|
16
|
+
*
|
|
17
|
+
* Cards open the per-collection settings (`/collections/:slug/settings`
|
|
18
|
+
* in the existing admin) when clicked. The dashed "+ New Post Type"
|
|
19
|
+
* card routes to /posts/types/new where users get instructions for
|
|
20
|
+
* adding a new collection to the config file.
|
|
21
|
+
*
|
|
22
|
+
* Why no in-app create/edit flow yet:
|
|
23
|
+
* Collections are file-defined (actuate.config.ts). Writing to that
|
|
24
|
+
* file at runtime would require a parallel data model and break the
|
|
25
|
+
* read-only deploy invariant on serverless platforms. v2 of this
|
|
26
|
+
* surface will introduce a DB-overlay for visual metadata (icon /
|
|
27
|
+
* color / description) — schema stays in code.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
interface PostTypesViewProps {
|
|
31
|
+
onNavigate: (path: string) => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function PostTypesView({ onNavigate }: PostTypesViewProps) {
|
|
35
|
+
const [types, setTypes] = useState<PostType[]>([])
|
|
36
|
+
const [loading, setLoading] = useState(true)
|
|
37
|
+
const [error, setError] = useState<string | null>(null)
|
|
38
|
+
const [refreshToken, setRefreshToken] = useState(0)
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
let cancelled = false
|
|
42
|
+
setLoading(true)
|
|
43
|
+
setError(null)
|
|
44
|
+
fetchPosts({ pageSize: 1 })
|
|
45
|
+
.then((res) => {
|
|
46
|
+
if (cancelled) return
|
|
47
|
+
setTypes(res.types)
|
|
48
|
+
setLoading(false)
|
|
49
|
+
})
|
|
50
|
+
.catch((err: unknown) => {
|
|
51
|
+
if (cancelled) return
|
|
52
|
+
setError(err instanceof Error ? err.message : 'Failed to load Post Types')
|
|
53
|
+
setLoading(false)
|
|
54
|
+
})
|
|
55
|
+
return () => {
|
|
56
|
+
cancelled = true
|
|
57
|
+
}
|
|
58
|
+
}, [refreshToken])
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex h-full flex-col gap-6 p-4 sm:p-6">
|
|
62
|
+
<div className="flex flex-col gap-1">
|
|
63
|
+
<h1 className="text-foreground text-xl font-medium tracking-tight sm:text-2xl">
|
|
64
|
+
Post Types
|
|
65
|
+
</h1>
|
|
66
|
+
<p className="text-muted-foreground text-sm">
|
|
67
|
+
Define content models and their fields. All types share the same default fields.
|
|
68
|
+
</p>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{loading && <CardGridSkeleton />}
|
|
72
|
+
{!loading && error && (
|
|
73
|
+
<div className="border-border bg-card rounded-xl border p-8 text-center">
|
|
74
|
+
<div className="text-foreground mb-2 text-sm font-medium">{error}</div>
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
onClick={() => setRefreshToken((t) => t + 1)}
|
|
78
|
+
className="text-primary text-sm hover:underline"
|
|
79
|
+
>
|
|
80
|
+
Try again
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
{!loading && !error && (
|
|
86
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
87
|
+
{types.map((t) => (
|
|
88
|
+
<PostTypeCard
|
|
89
|
+
key={t.slug}
|
|
90
|
+
type={t}
|
|
91
|
+
onClick={() => onNavigate(`/posts/types/${t.slug}/edit`)}
|
|
92
|
+
onEditTemplate={() => onNavigate(`/posts/types/${t.slug}/template`)}
|
|
93
|
+
/>
|
|
94
|
+
))}
|
|
95
|
+
<NewPostTypeCard onClick={() => onNavigate('/posts/types/new')} />
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── PostTypeCard ────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
function PostTypeCard({
|
|
105
|
+
type,
|
|
106
|
+
onClick,
|
|
107
|
+
onEditTemplate,
|
|
108
|
+
}: {
|
|
109
|
+
type: PostType
|
|
110
|
+
onClick: () => void
|
|
111
|
+
onEditTemplate: () => void
|
|
112
|
+
}) {
|
|
113
|
+
const palette = paletteFor(type.accentColor)
|
|
114
|
+
const Icon = resolvePostTypeIcon(type.icon)
|
|
115
|
+
// Tailwind needs the exact class string at build time — derive the
|
|
116
|
+
// top-border color from the palette's `border` token directly so it
|
|
117
|
+
// stays in sync with the badge color.
|
|
118
|
+
const topBorderClass = palette.border
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div className="bg-card border-border relative flex flex-col gap-3 overflow-hidden rounded-xl border p-5 shadow-sm transition-shadow hover:shadow-md">
|
|
122
|
+
<span aria-hidden className={`absolute inset-x-0 top-0 h-1 ${topBorderClass}`} />
|
|
123
|
+
{/* Main click target: manage posts of this type. */}
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
onClick={onClick}
|
|
127
|
+
aria-label={`Manage ${type.name} posts`}
|
|
128
|
+
className="focus-visible:ring-ring -m-1 flex flex-col gap-3 rounded-lg p-1 text-left focus:outline-none focus-visible:ring-2"
|
|
129
|
+
>
|
|
130
|
+
<div className="flex items-center gap-3 pt-2">
|
|
131
|
+
<span
|
|
132
|
+
className={`inline-flex h-9 w-9 items-center justify-center rounded-lg ${palette.bg} ${palette.text}`}
|
|
133
|
+
>
|
|
134
|
+
<Icon className="h-4 w-4" aria-hidden />
|
|
135
|
+
</span>
|
|
136
|
+
<h2 className="text-foreground text-base leading-tight font-medium">{type.name}</h2>
|
|
137
|
+
</div>
|
|
138
|
+
<p className="text-muted-foreground line-clamp-2 text-sm">
|
|
139
|
+
{type.description ?? 'No description provided.'}
|
|
140
|
+
</p>
|
|
141
|
+
</button>
|
|
142
|
+
<div className="border-border mt-auto flex flex-wrap items-center gap-x-4 gap-y-1 border-t pt-3 text-xs">
|
|
143
|
+
<span className="text-foreground font-medium tabular-nums">
|
|
144
|
+
{type.postCount} <span className="text-muted-foreground font-normal">posts</span>
|
|
145
|
+
</span>
|
|
146
|
+
<span className="text-foreground font-medium tabular-nums">
|
|
147
|
+
{type.fieldCount} <span className="text-muted-foreground font-normal">fields</span>
|
|
148
|
+
</span>
|
|
149
|
+
<span className="text-muted-foreground ml-auto truncate font-mono">{type.urlBase}</span>
|
|
150
|
+
</div>
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
onClick={onEditTemplate}
|
|
154
|
+
className="border-border text-foreground hover:bg-accent focus-visible:ring-ring inline-flex items-center justify-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors focus:outline-none focus-visible:ring-2"
|
|
155
|
+
>
|
|
156
|
+
<LayoutTemplate className="h-3.5 w-3.5" aria-hidden />
|
|
157
|
+
Edit template
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function NewPostTypeCard({ onClick }: { onClick: () => void }) {
|
|
164
|
+
return (
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
onClick={onClick}
|
|
168
|
+
aria-label="New Post Type"
|
|
169
|
+
className="border-border text-muted-foreground hover:text-foreground hover:border-primary/50 hover:bg-primary/5 focus-visible:ring-ring flex min-h-[170px] flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed bg-transparent p-5 transition-colors focus:outline-none focus-visible:ring-2"
|
|
170
|
+
>
|
|
171
|
+
<span className="bg-muted text-muted-foreground inline-flex h-9 w-9 items-center justify-center rounded-full">
|
|
172
|
+
<Plus className="h-4 w-4" aria-hidden />
|
|
173
|
+
</span>
|
|
174
|
+
<span className="text-sm font-medium">New Post Type</span>
|
|
175
|
+
</button>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function CardGridSkeleton() {
|
|
180
|
+
return (
|
|
181
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
182
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
183
|
+
<div
|
|
184
|
+
key={i}
|
|
185
|
+
className="bg-card border-border min-h-[170px] animate-pulse rounded-xl border p-5"
|
|
186
|
+
>
|
|
187
|
+
<div className="bg-muted mb-3 h-9 w-9 rounded-lg" />
|
|
188
|
+
<div className="bg-muted mb-2 h-4 w-1/2 rounded" />
|
|
189
|
+
<div className="bg-muted h-3 w-3/4 rounded" />
|
|
190
|
+
</div>
|
|
191
|
+
))}
|
|
192
|
+
</div>
|
|
193
|
+
)
|
|
194
|
+
}
|