@actuate-media/cms-admin 0.15.0 → 0.16.1
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__/lib/useApiData.test.d.ts +2 -0
- package/dist/__tests__/lib/useApiData.test.d.ts.map +1 -0
- package/dist/__tests__/lib/useApiData.test.js +84 -0
- package/dist/__tests__/lib/useApiData.test.js.map +1 -0
- package/dist/__tests__/views/dashboard.test.d.ts +2 -0
- package/dist/__tests__/views/dashboard.test.d.ts.map +1 -0
- package/dist/__tests__/views/dashboard.test.js +138 -0
- package/dist/__tests__/views/dashboard.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 +64 -20
- 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 +214 -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__/lib/useApiData.test.ts +105 -0
- package/src/__tests__/views/dashboard.test.tsx +180 -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 +142 -89
- 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,196 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { Modal } from '../../components/ui/Modal.js'
|
|
5
|
+
import { Button } from '../../components/ui/Button.js'
|
|
6
|
+
import type { EditorPost } from '../../lib/post-editor-service.js'
|
|
7
|
+
|
|
8
|
+
interface PostFieldsModalProps {
|
|
9
|
+
open: boolean
|
|
10
|
+
post: EditorPost
|
|
11
|
+
canEdit: boolean
|
|
12
|
+
onClose: () => void
|
|
13
|
+
onSave: (patch: Partial<EditorPost>) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const LABEL = 'text-foreground mb-1 block text-xs font-medium'
|
|
17
|
+
const INPUT =
|
|
18
|
+
'border-input bg-input-background text-foreground focus-visible:ring-ring w-full rounded-md border px-2.5 py-1.5 text-sm focus-visible:ring-2 focus-visible:outline-none'
|
|
19
|
+
|
|
20
|
+
function slugify(value: string): string {
|
|
21
|
+
return value
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.trim()
|
|
24
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
25
|
+
.replace(/^-+|-+$/g, '')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Edits post-level fields: title, slug, excerpt, featured image, category, body, SEO. */
|
|
29
|
+
export function PostFieldsModal({ open, post, canEdit, onClose, onSave }: PostFieldsModalProps) {
|
|
30
|
+
const [title, setTitle] = useState(post.title)
|
|
31
|
+
const [slug, setSlug] = useState(post.slug)
|
|
32
|
+
const [excerpt, setExcerpt] = useState(post.excerpt)
|
|
33
|
+
const [featuredImage, setFeaturedImage] = useState(post.featuredImage)
|
|
34
|
+
const [category, setCategory] = useState(post.category)
|
|
35
|
+
const [body, setBody] = useState(post.body)
|
|
36
|
+
const [seoTitle, setSeoTitle] = useState(post.seoTitle)
|
|
37
|
+
const [seoDescription, setSeoDescription] = useState(post.seoDescription)
|
|
38
|
+
const [slugTouched, setSlugTouched] = useState(Boolean(post.slug))
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!open) return
|
|
42
|
+
setTitle(post.title)
|
|
43
|
+
setSlug(post.slug)
|
|
44
|
+
setExcerpt(post.excerpt)
|
|
45
|
+
setFeaturedImage(post.featuredImage)
|
|
46
|
+
setCategory(post.category)
|
|
47
|
+
setBody(post.body)
|
|
48
|
+
setSeoTitle(post.seoTitle)
|
|
49
|
+
setSeoDescription(post.seoDescription)
|
|
50
|
+
setSlugTouched(Boolean(post.slug))
|
|
51
|
+
}, [open, post])
|
|
52
|
+
|
|
53
|
+
function handleTitle(next: string) {
|
|
54
|
+
setTitle(next)
|
|
55
|
+
if (!slugTouched) setSlug(slugify(next))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function handleSave() {
|
|
59
|
+
onSave({ title, slug, excerpt, featuredImage, category, body, seoTitle, seoDescription })
|
|
60
|
+
onClose()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Modal
|
|
65
|
+
open={open}
|
|
66
|
+
onClose={onClose}
|
|
67
|
+
title="Post details"
|
|
68
|
+
actions={
|
|
69
|
+
<>
|
|
70
|
+
<Button variant="ghost" onClick={onClose}>
|
|
71
|
+
Cancel
|
|
72
|
+
</Button>
|
|
73
|
+
<Button variant="primary" onClick={handleSave} disabled={!canEdit}>
|
|
74
|
+
Done
|
|
75
|
+
</Button>
|
|
76
|
+
</>
|
|
77
|
+
}
|
|
78
|
+
>
|
|
79
|
+
<div className="space-y-3">
|
|
80
|
+
<div>
|
|
81
|
+
<label className={LABEL} htmlFor="pf-title">
|
|
82
|
+
Title <span className="text-destructive">*</span>
|
|
83
|
+
</label>
|
|
84
|
+
<input
|
|
85
|
+
id="pf-title"
|
|
86
|
+
className={INPUT}
|
|
87
|
+
value={title}
|
|
88
|
+
disabled={!canEdit}
|
|
89
|
+
onChange={(e) => handleTitle(e.target.value)}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
<div className="grid grid-cols-2 gap-3">
|
|
93
|
+
<div>
|
|
94
|
+
<label className={LABEL} htmlFor="pf-slug">
|
|
95
|
+
Slug <span className="text-destructive">*</span>
|
|
96
|
+
</label>
|
|
97
|
+
<input
|
|
98
|
+
id="pf-slug"
|
|
99
|
+
className={INPUT}
|
|
100
|
+
value={slug}
|
|
101
|
+
disabled={!canEdit}
|
|
102
|
+
onChange={(e) => {
|
|
103
|
+
setSlugTouched(true)
|
|
104
|
+
setSlug(slugify(e.target.value))
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
<div>
|
|
109
|
+
<label className={LABEL} htmlFor="pf-category">
|
|
110
|
+
Category
|
|
111
|
+
</label>
|
|
112
|
+
<input
|
|
113
|
+
id="pf-category"
|
|
114
|
+
className={INPUT}
|
|
115
|
+
value={category}
|
|
116
|
+
disabled={!canEdit}
|
|
117
|
+
placeholder="e.g. Product"
|
|
118
|
+
onChange={(e) => setCategory(e.target.value)}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
<div>
|
|
123
|
+
<label className={LABEL} htmlFor="pf-excerpt">
|
|
124
|
+
Excerpt
|
|
125
|
+
</label>
|
|
126
|
+
<textarea
|
|
127
|
+
id="pf-excerpt"
|
|
128
|
+
className={`${INPUT} min-h-[60px] resize-y`}
|
|
129
|
+
value={excerpt}
|
|
130
|
+
disabled={!canEdit}
|
|
131
|
+
placeholder="A short summary shown in listings and the header."
|
|
132
|
+
onChange={(e) => setExcerpt(e.target.value)}
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
<div>
|
|
136
|
+
<label className={LABEL} htmlFor="pf-image">
|
|
137
|
+
Featured image URL
|
|
138
|
+
</label>
|
|
139
|
+
<input
|
|
140
|
+
id="pf-image"
|
|
141
|
+
className={INPUT}
|
|
142
|
+
value={featuredImage}
|
|
143
|
+
disabled={!canEdit}
|
|
144
|
+
placeholder="https://…"
|
|
145
|
+
onChange={(e) => setFeaturedImage(e.target.value)}
|
|
146
|
+
/>
|
|
147
|
+
{featuredImage && (
|
|
148
|
+
<div className="border-border bg-muted mt-2 overflow-hidden rounded-md border">
|
|
149
|
+
<img src={featuredImage} alt="" className="h-28 w-full object-cover" />
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
<div>
|
|
154
|
+
<label className={LABEL} htmlFor="pf-body">
|
|
155
|
+
Body
|
|
156
|
+
</label>
|
|
157
|
+
<textarea
|
|
158
|
+
id="pf-body"
|
|
159
|
+
className={`${INPUT} min-h-[120px] resize-y`}
|
|
160
|
+
value={body}
|
|
161
|
+
disabled={!canEdit}
|
|
162
|
+
placeholder="The main post content. Shown by the Article Body section."
|
|
163
|
+
onChange={(e) => setBody(e.target.value)}
|
|
164
|
+
/>
|
|
165
|
+
<p className="text-muted-foreground mt-1 text-xs">
|
|
166
|
+
Rendered by the Article Body section when its source is set to the post Body field.
|
|
167
|
+
</p>
|
|
168
|
+
</div>
|
|
169
|
+
<div>
|
|
170
|
+
<label className={LABEL} htmlFor="pf-seo-title">
|
|
171
|
+
SEO title
|
|
172
|
+
</label>
|
|
173
|
+
<input
|
|
174
|
+
id="pf-seo-title"
|
|
175
|
+
className={INPUT}
|
|
176
|
+
value={seoTitle}
|
|
177
|
+
disabled={!canEdit}
|
|
178
|
+
onChange={(e) => setSeoTitle(e.target.value)}
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
<div>
|
|
182
|
+
<label className={LABEL} htmlFor="pf-seo-desc">
|
|
183
|
+
SEO description
|
|
184
|
+
</label>
|
|
185
|
+
<textarea
|
|
186
|
+
id="pf-seo-desc"
|
|
187
|
+
className={`${INPUT} min-h-[60px] resize-y`}
|
|
188
|
+
value={seoDescription}
|
|
189
|
+
disabled={!canEdit}
|
|
190
|
+
onChange={(e) => setSeoDescription(e.target.value)}
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</Modal>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { PostHeaderConfig } from '../page-editor/section-types.js'
|
|
2
|
+
import type { PostRenderContext } from '../page-editor/sections/index.js'
|
|
3
|
+
|
|
4
|
+
interface PostHeaderProps {
|
|
5
|
+
config: PostHeaderConfig
|
|
6
|
+
context: PostRenderContext
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function formatDate(value?: string | null): string {
|
|
10
|
+
if (!value) return ''
|
|
11
|
+
const d = new Date(value)
|
|
12
|
+
if (Number.isNaN(d.getTime())) return ''
|
|
13
|
+
return d.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Renders a post's header region from the template's design config plus the
|
|
18
|
+
* post's own field values. Uses the same fixed website palette as the
|
|
19
|
+
* section renderers (see sections/parts.tsx) so the editor canvas and the
|
|
20
|
+
* public preview match.
|
|
21
|
+
*/
|
|
22
|
+
export function PostHeader({ config, context }: PostHeaderProps) {
|
|
23
|
+
const title = context.title || 'Untitled post'
|
|
24
|
+
const date = config.showDate ? formatDate(context.publishDate) : ''
|
|
25
|
+
const overlay =
|
|
26
|
+
config.layout === 'overlay' && config.showFeaturedImage && context.featuredImageUrl
|
|
27
|
+
const align = config.layout === 'centered' ? 'text-center mx-auto items-center' : 'text-left'
|
|
28
|
+
|
|
29
|
+
const meta = (
|
|
30
|
+
<div
|
|
31
|
+
className={`mt-4 flex flex-wrap gap-x-3 gap-y-1 text-sm ${
|
|
32
|
+
config.layout === 'centered' ? 'justify-center' : ''
|
|
33
|
+
} ${overlay ? 'text-white/80' : 'text-zinc-500'}`}
|
|
34
|
+
>
|
|
35
|
+
{config.showCategory && context.category && (
|
|
36
|
+
<span className="font-medium text-violet-500">{context.category}</span>
|
|
37
|
+
)}
|
|
38
|
+
{config.showAuthor && <span>{context.author?.name || 'Author name'}</span>}
|
|
39
|
+
{date && <span>{date}</span>}
|
|
40
|
+
{config.showReadingTime && <span>5 min read</span>}
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if (overlay) {
|
|
45
|
+
return (
|
|
46
|
+
<header className="relative">
|
|
47
|
+
<img src={context.featuredImageUrl} alt="" className="h-[360px] w-full object-cover" />
|
|
48
|
+
<div className="absolute inset-0 bg-gradient-to-t from-black/70 to-black/10" aria-hidden />
|
|
49
|
+
<div className="absolute inset-x-0 bottom-0 px-6 pb-8">
|
|
50
|
+
<div className={`mx-auto flex max-w-3xl flex-col ${align}`}>
|
|
51
|
+
<h1 className="text-3xl font-semibold tracking-tight text-white sm:text-4xl">
|
|
52
|
+
{title}
|
|
53
|
+
</h1>
|
|
54
|
+
{config.showExcerpt && context.excerpt && (
|
|
55
|
+
<p className="mt-3 max-w-2xl text-lg text-white/90">{context.excerpt}</p>
|
|
56
|
+
)}
|
|
57
|
+
{meta}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</header>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<header className="bg-white px-6 pt-12 pb-2">
|
|
66
|
+
<div
|
|
67
|
+
className={`flex max-w-3xl flex-col ${align} ${config.layout === 'centered' ? 'mx-auto' : ''}`}
|
|
68
|
+
>
|
|
69
|
+
{config.showCategory && context.category && config.layout !== 'centered' && null}
|
|
70
|
+
<h1 className="text-3xl font-semibold tracking-tight text-zinc-900 sm:text-4xl">{title}</h1>
|
|
71
|
+
{config.showExcerpt && context.excerpt && (
|
|
72
|
+
<p className="mt-3 max-w-2xl text-lg text-zinc-600">{context.excerpt}</p>
|
|
73
|
+
)}
|
|
74
|
+
{meta}
|
|
75
|
+
</div>
|
|
76
|
+
{config.showFeaturedImage && context.featuredImageUrl && (
|
|
77
|
+
<div className="mx-auto mt-8 max-w-4xl">
|
|
78
|
+
<img
|
|
79
|
+
src={context.featuredImageUrl}
|
|
80
|
+
alt=""
|
|
81
|
+
className="aspect-[16/9] w-full rounded-xl object-cover"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</header>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { X } from 'lucide-react'
|
|
4
|
+
import type { PostHeaderConfig } from '../../lib/post-editor-service.js'
|
|
5
|
+
|
|
6
|
+
interface PostHeaderPanelProps {
|
|
7
|
+
header: PostHeaderConfig
|
|
8
|
+
canEdit: boolean
|
|
9
|
+
onClose: () => void
|
|
10
|
+
onChange: (patch: Partial<PostHeaderConfig>) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const LABEL = 'text-foreground mb-1 block text-xs font-medium'
|
|
14
|
+
const INPUT =
|
|
15
|
+
'border-input bg-input-background text-foreground focus-visible:ring-ring w-full rounded-md border px-2.5 py-1.5 text-sm focus-visible:ring-2 focus-visible:outline-none'
|
|
16
|
+
|
|
17
|
+
const TOGGLES: Array<{ key: keyof PostHeaderConfig; label: string }> = [
|
|
18
|
+
{ key: 'showFeaturedImage', label: 'Featured image' },
|
|
19
|
+
{ key: 'showExcerpt', label: 'Excerpt' },
|
|
20
|
+
{ key: 'showAuthor', label: 'Author' },
|
|
21
|
+
{ key: 'showDate', label: 'Date' },
|
|
22
|
+
{ key: 'showCategory', label: 'Category' },
|
|
23
|
+
{ key: 'showReadingTime', label: 'Reading time' },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
/** Inspector panel for the post header layout (template-level design config). */
|
|
27
|
+
export function PostHeaderPanel({ header, canEdit, onClose, onChange }: PostHeaderPanelProps) {
|
|
28
|
+
return (
|
|
29
|
+
<aside
|
|
30
|
+
role="dialog"
|
|
31
|
+
aria-label="Edit post header"
|
|
32
|
+
className="bg-card border-border flex h-full w-80 shrink-0 flex-col border-l"
|
|
33
|
+
>
|
|
34
|
+
<div className="border-border flex items-center justify-between border-b px-4 py-3">
|
|
35
|
+
<div className="min-w-0">
|
|
36
|
+
<h2 className="text-foreground truncate text-sm font-medium">Post header</h2>
|
|
37
|
+
<p className="text-muted-foreground text-xs">Layout for every post of this type</p>
|
|
38
|
+
</div>
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
onClick={onClose}
|
|
42
|
+
aria-label="Close header panel"
|
|
43
|
+
className="text-muted-foreground hover:text-foreground hover:bg-accent rounded p-1"
|
|
44
|
+
>
|
|
45
|
+
<X className="h-4 w-4" aria-hidden />
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div className="flex-1 space-y-6 overflow-y-auto p-4">
|
|
50
|
+
<fieldset disabled={!canEdit} className="space-y-3">
|
|
51
|
+
<legend className="text-muted-foreground mb-2 text-[10px] font-semibold tracking-wider uppercase">
|
|
52
|
+
Layout
|
|
53
|
+
</legend>
|
|
54
|
+
<div>
|
|
55
|
+
<label className={LABEL} htmlFor="ph-layout">
|
|
56
|
+
Header style
|
|
57
|
+
</label>
|
|
58
|
+
<select
|
|
59
|
+
id="ph-layout"
|
|
60
|
+
className={INPUT}
|
|
61
|
+
value={header.layout}
|
|
62
|
+
onChange={(e) => onChange({ layout: e.target.value as PostHeaderConfig['layout'] })}
|
|
63
|
+
>
|
|
64
|
+
<option value="centered">Centered</option>
|
|
65
|
+
<option value="left">Left-aligned</option>
|
|
66
|
+
<option value="overlay">Image overlay</option>
|
|
67
|
+
</select>
|
|
68
|
+
</div>
|
|
69
|
+
</fieldset>
|
|
70
|
+
|
|
71
|
+
<fieldset disabled={!canEdit} className="space-y-2">
|
|
72
|
+
<legend className="text-muted-foreground mb-2 text-[10px] font-semibold tracking-wider uppercase">
|
|
73
|
+
Show in header
|
|
74
|
+
</legend>
|
|
75
|
+
{TOGGLES.map((t) => {
|
|
76
|
+
const value = header[t.key] === true
|
|
77
|
+
return (
|
|
78
|
+
<button
|
|
79
|
+
key={t.key}
|
|
80
|
+
type="button"
|
|
81
|
+
role="switch"
|
|
82
|
+
aria-checked={value}
|
|
83
|
+
disabled={!canEdit}
|
|
84
|
+
onClick={() => onChange({ [t.key]: !value } as Partial<PostHeaderConfig>)}
|
|
85
|
+
className="border-border text-foreground hover:bg-accent flex w-full items-center justify-between rounded-md border px-2.5 py-1.5 text-sm disabled:opacity-50"
|
|
86
|
+
>
|
|
87
|
+
<span>{t.label}</span>
|
|
88
|
+
<span
|
|
89
|
+
aria-hidden
|
|
90
|
+
className={`relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors ${
|
|
91
|
+
value ? 'bg-primary' : 'bg-muted'
|
|
92
|
+
}`}
|
|
93
|
+
>
|
|
94
|
+
<span
|
|
95
|
+
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
96
|
+
value ? 'translate-x-4' : 'translate-x-0.5'
|
|
97
|
+
}`}
|
|
98
|
+
/>
|
|
99
|
+
</span>
|
|
100
|
+
</button>
|
|
101
|
+
)
|
|
102
|
+
})}
|
|
103
|
+
</fieldset>
|
|
104
|
+
</div>
|
|
105
|
+
</aside>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { AlertTriangle, Loader2, Pencil, X } from 'lucide-react'
|
|
5
|
+
import {
|
|
6
|
+
fetchPostForEditor,
|
|
7
|
+
fetchTemplateForEditor,
|
|
8
|
+
type EditorPost,
|
|
9
|
+
type PostHeaderConfig,
|
|
10
|
+
} from '../../lib/post-editor-service.js'
|
|
11
|
+
import { defaultPostHeader } from '@actuate-media/cms-core/page-sections'
|
|
12
|
+
import { SectionRenderer, type PostRenderContext } from '../page-editor/sections/index.js'
|
|
13
|
+
import { PostHeader } from './PostHeader.js'
|
|
14
|
+
|
|
15
|
+
interface PostPreviewProps {
|
|
16
|
+
postType: string
|
|
17
|
+
documentId: string
|
|
18
|
+
session?: any
|
|
19
|
+
onNavigate: (path: string) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Chrome-free render of a post using the same header + section renderers as
|
|
24
|
+
* the editor canvas, so the preview reflects what ships. Only visible
|
|
25
|
+
* sections render (hidden ones are omitted, like the public page).
|
|
26
|
+
*/
|
|
27
|
+
export function PostPreview({ postType, documentId, session, onNavigate }: PostPreviewProps) {
|
|
28
|
+
const [post, setPost] = useState<EditorPost | null>(null)
|
|
29
|
+
const [header, setHeader] = useState<PostHeaderConfig>(defaultPostHeader())
|
|
30
|
+
const [loading, setLoading] = useState(true)
|
|
31
|
+
const [error, setError] = useState<string | null>(null)
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
let cancelled = false
|
|
35
|
+
setLoading(true)
|
|
36
|
+
fetchTemplateForEditor(postType)
|
|
37
|
+
.then((t) => !cancelled && setHeader(t.header))
|
|
38
|
+
.catch(() => {
|
|
39
|
+
/* default header */
|
|
40
|
+
})
|
|
41
|
+
fetchPostForEditor(postType, documentId)
|
|
42
|
+
.then((p) => !cancelled && setPost(p))
|
|
43
|
+
.catch((err) => !cancelled && setError(err instanceof Error ? err.message : 'Failed to load'))
|
|
44
|
+
.finally(() => !cancelled && setLoading(false))
|
|
45
|
+
return () => {
|
|
46
|
+
cancelled = true
|
|
47
|
+
}
|
|
48
|
+
}, [postType, documentId])
|
|
49
|
+
|
|
50
|
+
if (loading) {
|
|
51
|
+
return (
|
|
52
|
+
<div className="bg-background flex h-full items-center justify-center">
|
|
53
|
+
<Loader2 className="text-muted-foreground h-6 w-6 animate-spin" aria-hidden />
|
|
54
|
+
<span className="sr-only">Loading preview…</span>
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (error || !post) {
|
|
60
|
+
return (
|
|
61
|
+
<div className="bg-background flex h-full items-center justify-center p-6">
|
|
62
|
+
<div className="border-destructive/30 bg-destructive/5 flex max-w-md items-center gap-3 rounded-lg border p-4">
|
|
63
|
+
<AlertTriangle className="text-destructive shrink-0" size={20} aria-hidden />
|
|
64
|
+
<p className="text-foreground text-sm font-medium">{error ?? 'Post not found.'}</p>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const context: PostRenderContext = {
|
|
71
|
+
title: post.title,
|
|
72
|
+
excerpt: post.excerpt,
|
|
73
|
+
body: post.body,
|
|
74
|
+
featuredImageUrl: post.featuredImage,
|
|
75
|
+
publishDate: post.publishDate ?? post.updatedAt ?? null,
|
|
76
|
+
category: post.category,
|
|
77
|
+
author: { name: session?.user?.name || session?.user?.email },
|
|
78
|
+
}
|
|
79
|
+
const visible = post.sections.filter((s) => s.visible)
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="flex h-full flex-col">
|
|
83
|
+
{/* Slim preview bar. This view opens in its own browser tab, so
|
|
84
|
+
actions are tab-scoped. */}
|
|
85
|
+
<div className="bg-card border-border flex h-12 shrink-0 items-center justify-between border-b px-4">
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
onClick={() => window.close()}
|
|
89
|
+
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-sm"
|
|
90
|
+
aria-label="Close preview tab"
|
|
91
|
+
>
|
|
92
|
+
<X className="h-4 w-4" aria-hidden />
|
|
93
|
+
Close
|
|
94
|
+
</button>
|
|
95
|
+
<span className="text-muted-foreground text-xs">Preview · {post.title || 'Untitled'}</span>
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
onClick={() => onNavigate(`/posts/${postType}/${documentId}/edit`)}
|
|
99
|
+
className="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium"
|
|
100
|
+
>
|
|
101
|
+
<Pencil className="h-3.5 w-3.5" aria-hidden />
|
|
102
|
+
Open editor
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div className="flex-1 overflow-auto bg-white">
|
|
107
|
+
<PostHeader config={header} context={context} />
|
|
108
|
+
{visible.map((section) => (
|
|
109
|
+
<SectionRenderer key={section.id} section={section} context={context} />
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
}
|