@prenta/admin 0.98.1 → 1.0.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.
Files changed (267) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/AdminRoot.d.ts.map +1 -1
  3. package/dist/AdminRoot.js +17 -28
  4. package/dist/AdminRoot.js.map +1 -1
  5. package/dist/__tests__/components/seo-score-bands.test.js +0 -3
  6. package/dist/__tests__/components/seo-score-bands.test.js.map +1 -1
  7. package/dist/__tests__/layout/sidebar-posts.test.js +6 -11
  8. package/dist/__tests__/layout/sidebar-posts.test.js.map +1 -1
  9. package/dist/__tests__/layout/sidebar-submenu.render.test.js +3 -2
  10. package/dist/__tests__/layout/sidebar-submenu.render.test.js.map +1 -1
  11. package/dist/__tests__/lib/page-editor-service-api.test.js +26 -23
  12. package/dist/__tests__/lib/page-editor-service-api.test.js.map +1 -1
  13. package/dist/__tests__/views/page-section-editor.test.js +0 -1
  14. package/dist/__tests__/views/page-section-editor.test.js.map +1 -1
  15. package/dist/__tests__/views/page-tab.render.test.js +0 -1
  16. package/dist/__tests__/views/page-tab.render.test.js.map +1 -1
  17. package/dist/index.d.ts +1 -4
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -3
  20. package/dist/index.js.map +1 -1
  21. package/dist/layout/Sidebar.d.ts +3 -4
  22. package/dist/layout/Sidebar.d.ts.map +1 -1
  23. package/dist/layout/Sidebar.js +7 -38
  24. package/dist/layout/Sidebar.js.map +1 -1
  25. package/dist/lib/page-editor-service.d.ts +0 -7
  26. package/dist/lib/page-editor-service.d.ts.map +1 -1
  27. package/dist/lib/page-editor-service.js +9 -9
  28. package/dist/lib/page-editor-service.js.map +1 -1
  29. package/dist/prenta-admin.css +1 -1
  30. package/dist/views/PageEditor.d.ts +2 -2
  31. package/dist/views/PageEditor.js +2 -2
  32. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  33. package/dist/views/page-editor/PageSectionEditor.js +9 -15
  34. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  35. package/package.json +4 -4
  36. package/src/AdminRoot.tsx +18 -68
  37. package/src/__tests__/components/seo-score-bands.test.ts +0 -3
  38. package/src/__tests__/layout/sidebar-posts.test.ts +6 -11
  39. package/src/__tests__/layout/sidebar-submenu.render.test.tsx +3 -2
  40. package/src/__tests__/lib/page-editor-service-api.test.ts +27 -25
  41. package/src/__tests__/views/page-section-editor.test.tsx +0 -1
  42. package/src/__tests__/views/page-tab.render.test.tsx +0 -1
  43. package/src/index.ts +1 -11
  44. package/src/layout/Sidebar.tsx +6 -41
  45. package/src/lib/page-editor-service.ts +9 -16
  46. package/src/views/PageEditor.tsx +2 -2
  47. package/src/views/page-editor/PageSectionEditor.tsx +8 -37
  48. package/dist/__tests__/views/page-builder-block-renderer.render.test.d.ts +0 -2
  49. package/dist/__tests__/views/page-builder-block-renderer.render.test.d.ts.map +0 -1
  50. package/dist/__tests__/views/page-builder-block-renderer.render.test.js +0 -48
  51. package/dist/__tests__/views/page-builder-block-renderer.render.test.js.map +0 -1
  52. package/dist/__tests__/views/page-builder-form-picker.render.test.d.ts +0 -2
  53. package/dist/__tests__/views/page-builder-form-picker.render.test.d.ts.map +0 -1
  54. package/dist/__tests__/views/page-builder-form-picker.render.test.js +0 -44
  55. package/dist/__tests__/views/page-builder-form-picker.render.test.js.map +0 -1
  56. package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts +0 -2
  57. package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts.map +0 -1
  58. package/dist/__tests__/views/page-builder-sections-panel.render.test.js +0 -121
  59. package/dist/__tests__/views/page-builder-sections-panel.render.test.js.map +0 -1
  60. package/dist/__tests__/views/page-builder-seo-panel.render.test.d.ts +0 -2
  61. package/dist/__tests__/views/page-builder-seo-panel.render.test.d.ts.map +0 -1
  62. package/dist/__tests__/views/page-builder-seo-panel.render.test.js +0 -65
  63. package/dist/__tests__/views/page-builder-seo-panel.render.test.js.map +0 -1
  64. package/dist/__tests__/views/page-builder-template-seed.render.test.d.ts +0 -2
  65. package/dist/__tests__/views/page-builder-template-seed.render.test.d.ts.map +0 -1
  66. package/dist/__tests__/views/page-builder-template-seed.render.test.js +0 -48
  67. package/dist/__tests__/views/page-builder-template-seed.render.test.js.map +0 -1
  68. package/dist/__tests__/views/page-templates.render.test.d.ts +0 -2
  69. package/dist/__tests__/views/page-templates.render.test.d.ts.map +0 -1
  70. package/dist/__tests__/views/page-templates.render.test.js +0 -126
  71. package/dist/__tests__/views/page-templates.render.test.js.map +0 -1
  72. package/dist/hooks/useBuilderState.d.ts +0 -49
  73. package/dist/hooks/useBuilderState.d.ts.map +0 -1
  74. package/dist/hooks/useBuilderState.js +0 -238
  75. package/dist/hooks/useBuilderState.js.map +0 -1
  76. package/dist/views/page-builder/AIBlockAssist.d.ts +0 -9
  77. package/dist/views/page-builder/AIBlockAssist.d.ts.map +0 -1
  78. package/dist/views/page-builder/AIBlockAssist.js +0 -40
  79. package/dist/views/page-builder/AIBlockAssist.js.map +0 -1
  80. package/dist/views/page-builder/AIGenerateDialog.d.ts +0 -8
  81. package/dist/views/page-builder/AIGenerateDialog.d.ts.map +0 -1
  82. package/dist/views/page-builder/AIGenerateDialog.js +0 -173
  83. package/dist/views/page-builder/AIGenerateDialog.js.map +0 -1
  84. package/dist/views/page-builder/BlockEditor.d.ts +0 -11
  85. package/dist/views/page-builder/BlockEditor.d.ts.map +0 -1
  86. package/dist/views/page-builder/BlockEditor.js +0 -159
  87. package/dist/views/page-builder/BlockEditor.js.map +0 -1
  88. package/dist/views/page-builder/BlockPicker.d.ts +0 -7
  89. package/dist/views/page-builder/BlockPicker.d.ts.map +0 -1
  90. package/dist/views/page-builder/BlockPicker.js +0 -103
  91. package/dist/views/page-builder/BlockPicker.js.map +0 -1
  92. package/dist/views/page-builder/BottomBar.d.ts +0 -9
  93. package/dist/views/page-builder/BottomBar.d.ts.map +0 -1
  94. package/dist/views/page-builder/BottomBar.js +0 -13
  95. package/dist/views/page-builder/BottomBar.js.map +0 -1
  96. package/dist/views/page-builder/BuilderSectionsPanel.d.ts +0 -20
  97. package/dist/views/page-builder/BuilderSectionsPanel.d.ts.map +0 -1
  98. package/dist/views/page-builder/BuilderSectionsPanel.js +0 -105
  99. package/dist/views/page-builder/BuilderSectionsPanel.js.map +0 -1
  100. package/dist/views/page-builder/BuilderToolbar.d.ts +0 -21
  101. package/dist/views/page-builder/BuilderToolbar.d.ts.map +0 -1
  102. package/dist/views/page-builder/BuilderToolbar.js +0 -18
  103. package/dist/views/page-builder/BuilderToolbar.js.map +0 -1
  104. package/dist/views/page-builder/ContextPanel.d.ts +0 -20
  105. package/dist/views/page-builder/ContextPanel.d.ts.map +0 -1
  106. package/dist/views/page-builder/ContextPanel.js +0 -43
  107. package/dist/views/page-builder/ContextPanel.js.map +0 -1
  108. package/dist/views/page-builder/DesignScore.d.ts +0 -6
  109. package/dist/views/page-builder/DesignScore.d.ts.map +0 -1
  110. package/dist/views/page-builder/DesignScore.js +0 -93
  111. package/dist/views/page-builder/DesignScore.js.map +0 -1
  112. package/dist/views/page-builder/NodeSettings.d.ts +0 -12
  113. package/dist/views/page-builder/NodeSettings.d.ts.map +0 -1
  114. package/dist/views/page-builder/NodeSettings.js +0 -80
  115. package/dist/views/page-builder/NodeSettings.js.map +0 -1
  116. package/dist/views/page-builder/PageBuilder.d.ts +0 -10
  117. package/dist/views/page-builder/PageBuilder.d.ts.map +0 -1
  118. package/dist/views/page-builder/PageBuilder.js +0 -222
  119. package/dist/views/page-builder/PageBuilder.js.map +0 -1
  120. package/dist/views/page-builder/PageSettings.d.ts +0 -7
  121. package/dist/views/page-builder/PageSettings.d.ts.map +0 -1
  122. package/dist/views/page-builder/PageSettings.js +0 -27
  123. package/dist/views/page-builder/PageSettings.js.map +0 -1
  124. package/dist/views/page-builder/PageTemplates.d.ts +0 -5
  125. package/dist/views/page-builder/PageTemplates.d.ts.map +0 -1
  126. package/dist/views/page-builder/PageTemplates.js +0 -146
  127. package/dist/views/page-builder/PageTemplates.js.map +0 -1
  128. package/dist/views/page-builder/RelationshipField.d.ts +0 -15
  129. package/dist/views/page-builder/RelationshipField.d.ts.map +0 -1
  130. package/dist/views/page-builder/RelationshipField.js +0 -65
  131. package/dist/views/page-builder/RelationshipField.js.map +0 -1
  132. package/dist/views/page-builder/SEOPanel.d.ts +0 -9
  133. package/dist/views/page-builder/SEOPanel.d.ts.map +0 -1
  134. package/dist/views/page-builder/SEOPanel.js +0 -38
  135. package/dist/views/page-builder/SEOPanel.js.map +0 -1
  136. package/dist/views/page-builder/SavedSections.d.ts +0 -6
  137. package/dist/views/page-builder/SavedSections.d.ts.map +0 -1
  138. package/dist/views/page-builder/SavedSections.js +0 -141
  139. package/dist/views/page-builder/SavedSections.js.map +0 -1
  140. package/dist/views/page-builder/TemplatePicker.d.ts +0 -7
  141. package/dist/views/page-builder/TemplatePicker.d.ts.map +0 -1
  142. package/dist/views/page-builder/TemplatePicker.js +0 -68
  143. package/dist/views/page-builder/TemplatePicker.js.map +0 -1
  144. package/dist/views/page-builder/Widgets.d.ts +0 -13
  145. package/dist/views/page-builder/Widgets.d.ts.map +0 -1
  146. package/dist/views/page-builder/Widgets.js +0 -15
  147. package/dist/views/page-builder/Widgets.js.map +0 -1
  148. package/dist/views/page-builder/block-renderers/CTAPreview.d.ts +0 -3
  149. package/dist/views/page-builder/block-renderers/CTAPreview.d.ts.map +0 -1
  150. package/dist/views/page-builder/block-renderers/CTAPreview.js +0 -19
  151. package/dist/views/page-builder/block-renderers/CTAPreview.js.map +0 -1
  152. package/dist/views/page-builder/block-renderers/CardsPreview.d.ts +0 -3
  153. package/dist/views/page-builder/block-renderers/CardsPreview.d.ts.map +0 -1
  154. package/dist/views/page-builder/block-renderers/CardsPreview.js +0 -22
  155. package/dist/views/page-builder/block-renderers/CardsPreview.js.map +0 -1
  156. package/dist/views/page-builder/block-renderers/CodePreview.d.ts +0 -3
  157. package/dist/views/page-builder/block-renderers/CodePreview.d.ts.map +0 -1
  158. package/dist/views/page-builder/block-renderers/CodePreview.js +0 -12
  159. package/dist/views/page-builder/block-renderers/CodePreview.js.map +0 -1
  160. package/dist/views/page-builder/block-renderers/FAQPreview.d.ts +0 -3
  161. package/dist/views/page-builder/block-renderers/FAQPreview.d.ts.map +0 -1
  162. package/dist/views/page-builder/block-renderers/FAQPreview.js +0 -27
  163. package/dist/views/page-builder/block-renderers/FAQPreview.js.map +0 -1
  164. package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts +0 -6
  165. package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts.map +0 -1
  166. package/dist/views/page-builder/block-renderers/FallbackPreview.js +0 -7
  167. package/dist/views/page-builder/block-renderers/FallbackPreview.js.map +0 -1
  168. package/dist/views/page-builder/block-renderers/FormPreview.d.ts +0 -9
  169. package/dist/views/page-builder/block-renderers/FormPreview.d.ts.map +0 -1
  170. package/dist/views/page-builder/block-renderers/FormPreview.js +0 -52
  171. package/dist/views/page-builder/block-renderers/FormPreview.js.map +0 -1
  172. package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts +0 -3
  173. package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts.map +0 -1
  174. package/dist/views/page-builder/block-renderers/GalleryPreview.js +0 -19
  175. package/dist/views/page-builder/block-renderers/GalleryPreview.js.map +0 -1
  176. package/dist/views/page-builder/block-renderers/HeroPreview.d.ts +0 -3
  177. package/dist/views/page-builder/block-renderers/HeroPreview.d.ts.map +0 -1
  178. package/dist/views/page-builder/block-renderers/HeroPreview.js +0 -19
  179. package/dist/views/page-builder/block-renderers/HeroPreview.js.map +0 -1
  180. package/dist/views/page-builder/block-renderers/ImagePreview.d.ts +0 -3
  181. package/dist/views/page-builder/block-renderers/ImagePreview.d.ts.map +0 -1
  182. package/dist/views/page-builder/block-renderers/ImagePreview.js +0 -17
  183. package/dist/views/page-builder/block-renderers/ImagePreview.js.map +0 -1
  184. package/dist/views/page-builder/block-renderers/TextPreview.d.ts +0 -3
  185. package/dist/views/page-builder/block-renderers/TextPreview.d.ts.map +0 -1
  186. package/dist/views/page-builder/block-renderers/TextPreview.js +0 -22
  187. package/dist/views/page-builder/block-renderers/TextPreview.js.map +0 -1
  188. package/dist/views/page-builder/block-renderers/VideoPreview.d.ts +0 -3
  189. package/dist/views/page-builder/block-renderers/VideoPreview.d.ts.map +0 -1
  190. package/dist/views/page-builder/block-renderers/VideoPreview.js +0 -18
  191. package/dist/views/page-builder/block-renderers/VideoPreview.js.map +0 -1
  192. package/dist/views/page-builder/block-renderers/index.d.ts +0 -9
  193. package/dist/views/page-builder/block-renderers/index.d.ts.map +0 -1
  194. package/dist/views/page-builder/block-renderers/index.js +0 -25
  195. package/dist/views/page-builder/block-renderers/index.js.map +0 -1
  196. package/dist/views/page-builder/canvas/BlockRenderer.d.ts +0 -8
  197. package/dist/views/page-builder/canvas/BlockRenderer.d.ts.map +0 -1
  198. package/dist/views/page-builder/canvas/BlockRenderer.js +0 -20
  199. package/dist/views/page-builder/canvas/BlockRenderer.js.map +0 -1
  200. package/dist/views/page-builder/canvas/BuilderCanvas.d.ts +0 -10
  201. package/dist/views/page-builder/canvas/BuilderCanvas.d.ts.map +0 -1
  202. package/dist/views/page-builder/canvas/BuilderCanvas.js +0 -26
  203. package/dist/views/page-builder/canvas/BuilderCanvas.js.map +0 -1
  204. package/dist/views/page-builder/canvas/ColumnRenderer.d.ts +0 -8
  205. package/dist/views/page-builder/canvas/ColumnRenderer.d.ts.map +0 -1
  206. package/dist/views/page-builder/canvas/ColumnRenderer.js +0 -32
  207. package/dist/views/page-builder/canvas/ColumnRenderer.js.map +0 -1
  208. package/dist/views/page-builder/canvas/ContainerRenderer.d.ts +0 -8
  209. package/dist/views/page-builder/canvas/ContainerRenderer.d.ts.map +0 -1
  210. package/dist/views/page-builder/canvas/ContainerRenderer.js +0 -29
  211. package/dist/views/page-builder/canvas/ContainerRenderer.js.map +0 -1
  212. package/dist/views/page-builder/canvas/RowRenderer.d.ts +0 -8
  213. package/dist/views/page-builder/canvas/RowRenderer.d.ts.map +0 -1
  214. package/dist/views/page-builder/canvas/RowRenderer.js +0 -28
  215. package/dist/views/page-builder/canvas/RowRenderer.js.map +0 -1
  216. package/dist/views/page-builder/canvas/SectionRenderer.d.ts +0 -8
  217. package/dist/views/page-builder/canvas/SectionRenderer.d.ts.map +0 -1
  218. package/dist/views/page-builder/canvas/SectionRenderer.js +0 -50
  219. package/dist/views/page-builder/canvas/SectionRenderer.js.map +0 -1
  220. package/dist/views/page-builder/canvas/index.d.ts +0 -3
  221. package/dist/views/page-builder/canvas/index.d.ts.map +0 -1
  222. package/dist/views/page-builder/canvas/index.js +0 -2
  223. package/dist/views/page-builder/canvas/index.js.map +0 -1
  224. package/src/__tests__/views/page-builder-block-renderer.render.test.tsx +0 -66
  225. package/src/__tests__/views/page-builder-form-picker.render.test.tsx +0 -54
  226. package/src/__tests__/views/page-builder-sections-panel.render.test.tsx +0 -138
  227. package/src/__tests__/views/page-builder-seo-panel.render.test.tsx +0 -77
  228. package/src/__tests__/views/page-builder-template-seed.render.test.tsx +0 -59
  229. package/src/__tests__/views/page-templates.render.test.tsx +0 -182
  230. package/src/hooks/useBuilderState.ts +0 -361
  231. package/src/views/page-builder/AIBlockAssist.tsx +0 -64
  232. package/src/views/page-builder/AIGenerateDialog.tsx +0 -593
  233. package/src/views/page-builder/BlockEditor.tsx +0 -523
  234. package/src/views/page-builder/BlockPicker.tsx +0 -324
  235. package/src/views/page-builder/BottomBar.tsx +0 -65
  236. package/src/views/page-builder/BuilderSectionsPanel.tsx +0 -304
  237. package/src/views/page-builder/BuilderToolbar.tsx +0 -221
  238. package/src/views/page-builder/ContextPanel.tsx +0 -144
  239. package/src/views/page-builder/DesignScore.tsx +0 -251
  240. package/src/views/page-builder/NodeSettings.tsx +0 -500
  241. package/src/views/page-builder/PageBuilder.tsx +0 -405
  242. package/src/views/page-builder/PageSettings.tsx +0 -162
  243. package/src/views/page-builder/PageTemplates.tsx +0 -435
  244. package/src/views/page-builder/RelationshipField.tsx +0 -113
  245. package/src/views/page-builder/SEOPanel.tsx +0 -222
  246. package/src/views/page-builder/SavedSections.tsx +0 -480
  247. package/src/views/page-builder/TemplatePicker.tsx +0 -197
  248. package/src/views/page-builder/Widgets.tsx +0 -44
  249. package/src/views/page-builder/block-renderers/CTAPreview.tsx +0 -79
  250. package/src/views/page-builder/block-renderers/CardsPreview.tsx +0 -69
  251. package/src/views/page-builder/block-renderers/CodePreview.tsx +0 -46
  252. package/src/views/page-builder/block-renderers/FAQPreview.tsx +0 -87
  253. package/src/views/page-builder/block-renderers/FallbackPreview.tsx +0 -18
  254. package/src/views/page-builder/block-renderers/FormPreview.tsx +0 -117
  255. package/src/views/page-builder/block-renderers/GalleryPreview.tsx +0 -87
  256. package/src/views/page-builder/block-renderers/HeroPreview.tsx +0 -90
  257. package/src/views/page-builder/block-renderers/ImagePreview.tsx +0 -54
  258. package/src/views/page-builder/block-renderers/TextPreview.tsx +0 -81
  259. package/src/views/page-builder/block-renderers/VideoPreview.tsx +0 -73
  260. package/src/views/page-builder/block-renderers/index.ts +0 -34
  261. package/src/views/page-builder/canvas/BlockRenderer.tsx +0 -56
  262. package/src/views/page-builder/canvas/BuilderCanvas.tsx +0 -87
  263. package/src/views/page-builder/canvas/ColumnRenderer.tsx +0 -82
  264. package/src/views/page-builder/canvas/ContainerRenderer.tsx +0 -67
  265. package/src/views/page-builder/canvas/RowRenderer.tsx +0 -68
  266. package/src/views/page-builder/canvas/SectionRenderer.tsx +0 -93
  267. package/src/views/page-builder/canvas/index.ts +0 -2
@@ -1,435 +0,0 @@
1
- 'use client'
2
-
3
- import { useMemo, useState } from 'react'
4
- import { AlertTriangle, Copy, FilePlus, Layers, Pencil, RefreshCw, Trash2 } from 'lucide-react'
5
- import { toast } from 'sonner'
6
- import { cmsApi } from '../../lib/api.js'
7
- import { useApiData } from '../../lib/useApiData.js'
8
- import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
9
- import { EmptyState } from '../../components/ui/EmptyState.js'
10
- import { FilterPill } from '../../components/ui/FilterPill.js'
11
- import { Input } from '../../components/ui/Input.js'
12
- import { Modal } from '../../components/ui/Modal.js'
13
- import { Button } from '../../components/ui/Button.js'
14
- import { RowActions, RowActionButton } from '../../components/ui/RowActions.js'
15
- import { SearchInput } from '../../components/ui/SearchInput.js'
16
- import { Select } from '../../components/ui/Select.js'
17
- import { Skeleton } from '../../components/ui/Skeleton.js'
18
-
19
- interface PageTemplate {
20
- id: string
21
- name?: string
22
- description?: string | null
23
- category?: string
24
- builtIn?: boolean
25
- updatedAt?: string
26
- thumbnail?: string | null
27
- tree?: { children?: unknown[] } | null
28
- }
29
-
30
- type CategoryId = 'content' | 'landing' | 'utility'
31
-
32
- const CATEGORIES: Record<CategoryId, { label: string; dot: string; text: string }> = {
33
- content: { label: 'Content', dot: 'bg-[var(--acc)]', text: 'text-[var(--acc)]' },
34
- landing: { label: 'Landing', dot: 'bg-[var(--suc)]', text: 'text-[var(--suc)]' },
35
- utility: { label: 'Utility', dot: 'bg-[var(--warn)]', text: 'text-[var(--warn)]' },
36
- }
37
-
38
- function categoryOf(template: PageTemplate): CategoryId {
39
- const cat = template.category
40
- return cat === 'landing' || cat === 'utility' ? cat : 'content'
41
- }
42
-
43
- function sectionCountOf(template: PageTemplate): number {
44
- return Array.isArray(template.tree?.children) ? template.tree.children.length : 0
45
- }
46
-
47
- export interface PageTemplatesProps {
48
- onNavigate?: (path: string) => void
49
- }
50
-
51
- export function PageTemplates({ onNavigate }: PageTemplatesProps) {
52
- const { data, loading, error, refetch } = useApiData<PageTemplate[]>('/page-templates')
53
- const templates = useMemo(() => data ?? [], [data])
54
-
55
- const [search, setSearch] = useState('')
56
- const [category, setCategory] = useState<CategoryId | 'all'>('all')
57
- const [editTarget, setEditTarget] = useState<PageTemplate | null>(null)
58
- const [deleteTarget, setDeleteTarget] = useState<PageTemplate | null>(null)
59
-
60
- const filtered = useMemo(() => {
61
- const q = search.trim().toLowerCase()
62
- return templates.filter((t) => {
63
- const matchesQuery =
64
- !q ||
65
- (t.name ?? '').toLowerCase().includes(q) ||
66
- (t.description ?? '').toLowerCase().includes(q)
67
- const matchesCategory = category === 'all' || categoryOf(t) === category
68
- return matchesQuery && matchesCategory
69
- })
70
- }, [templates, search, category])
71
-
72
- async function confirmDelete() {
73
- if (!deleteTarget) return
74
- const res = await cmsApi(`/page-templates/${deleteTarget.id}`, { method: 'DELETE' })
75
- if (res.error) {
76
- toast.error(res.error)
77
- } else {
78
- toast.success('Template deleted')
79
- refetch()
80
- }
81
- }
82
-
83
- async function duplicateTemplate(template: PageTemplate) {
84
- // The list payload carries the full tree, so no extra fetch is needed.
85
- // Duplicating a built-in is the supported way to customize one (built-ins
86
- // themselves are locked against edits).
87
- if (!template.tree) {
88
- toast.error('Template has no layout to duplicate')
89
- return
90
- }
91
- const res = await cmsApi('/page-templates', {
92
- method: 'POST',
93
- body: JSON.stringify({
94
- name: `${template.name ?? 'Untitled template'} (copy)`,
95
- description: template.description ?? null,
96
- category: categoryOf(template),
97
- tree: template.tree,
98
- thumbnail: template.thumbnail ?? null,
99
- }),
100
- })
101
- if (res.error) {
102
- toast.error(res.error)
103
- } else {
104
- toast.success('Template duplicated')
105
- refetch()
106
- }
107
- }
108
-
109
- return (
110
- <div className="p-4 pr-8">
111
- {error && (
112
- <div className="mb-4 flex items-center gap-3 rounded-[var(--r)] border border-[var(--bdr)] bg-[var(--card)] p-3">
113
- <AlertTriangle className="h-5 w-5 shrink-0 text-[var(--muted)]" aria-hidden />
114
- <span className="flex-1 text-sm text-[var(--txt)]">{error}</span>
115
- <button
116
- type="button"
117
- onClick={refetch}
118
- className="rounded-md border border-[var(--bdr)] px-3 py-1 text-sm text-[var(--txt)] hover:bg-[var(--acc-l)]"
119
- >
120
- Retry
121
- </button>
122
- </div>
123
- )}
124
-
125
- <div className="mb-4 flex items-center justify-between gap-4">
126
- <div>
127
- <h1 className="text-foreground mb-1">Page Templates</h1>
128
- <p className="text-sm text-[var(--sub)]">
129
- {templates.length} saved template{templates.length === 1 ? '' : 's'}
130
- </p>
131
- </div>
132
- <Button
133
- type="button"
134
- variant="outline"
135
- size="md"
136
- onClick={refetch}
137
- leftIcon={<RefreshCw size={16} aria-hidden />}
138
- >
139
- Refresh
140
- </Button>
141
- </div>
142
-
143
- {!loading && templates.length > 0 && (
144
- <div className="mb-4 flex flex-wrap items-center gap-2">
145
- <div className="max-w-xs min-w-52 flex-1">
146
- <SearchInput value={search} onChange={setSearch} placeholder="Search templates…" />
147
- </div>
148
- <div className="flex gap-1.5" role="group" aria-label="Filter templates by category">
149
- <FilterPill active={category === 'all'} onClick={() => setCategory('all')}>
150
- All
151
- </FilterPill>
152
- {(Object.keys(CATEGORIES) as CategoryId[]).map((cat) => (
153
- <FilterPill key={cat} active={category === cat} onClick={() => setCategory(cat)}>
154
- {CATEGORIES[cat].label}
155
- </FilterPill>
156
- ))}
157
- </div>
158
- <span className="ml-auto text-[12.5px] whitespace-nowrap text-[var(--muted)]">
159
- {filtered.length} of {templates.length}
160
- </span>
161
- </div>
162
- )}
163
-
164
- {loading ? (
165
- <div
166
- className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3"
167
- role="status"
168
- aria-live="polite"
169
- aria-label="Loading page templates"
170
- >
171
- {Array.from({ length: 6 }, (_, i) => (
172
- <Skeleton key={i} variant="card" />
173
- ))}
174
- </div>
175
- ) : templates.length === 0 ? (
176
- <EmptyState
177
- icon={<Layers size={24} aria-hidden />}
178
- title="No page templates yet"
179
- description="Built-in templates are seeded by the CMS when the templates API is available."
180
- actionLabel="View Saved Sections"
181
- onAction={() => onNavigate?.('/saved-sections')}
182
- />
183
- ) : filtered.length === 0 ? (
184
- <p className="py-12 text-center text-sm text-[var(--muted)]">
185
- No templates match your search.
186
- </p>
187
- ) : (
188
- <ul
189
- className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3"
190
- aria-label="Page templates"
191
- >
192
- {filtered.map((template) => (
193
- <TemplateCard
194
- key={template.id}
195
- template={template}
196
- onUse={() => onNavigate?.(`/page-builder/new?template=${template.id}`)}
197
- onDuplicate={() => void duplicateTemplate(template)}
198
- onEdit={() => setEditTarget(template)}
199
- onDelete={() => setDeleteTarget(template)}
200
- />
201
- ))}
202
- </ul>
203
- )}
204
-
205
- <TemplateEditModal
206
- template={editTarget}
207
- onClose={() => setEditTarget(null)}
208
- onSaved={() => {
209
- setEditTarget(null)
210
- refetch()
211
- }}
212
- />
213
-
214
- <ConfirmDialog
215
- open={deleteTarget !== null}
216
- onClose={() => setDeleteTarget(null)}
217
- onConfirm={() => void confirmDelete()}
218
- title="Delete template?"
219
- description={`"${deleteTarget?.name ?? 'This template'}" will be permanently removed. Pages already created from it are not affected.`}
220
- confirmLabel="Delete"
221
- destructive
222
- />
223
- </div>
224
- )
225
- }
226
-
227
- function TemplateCard({
228
- template,
229
- onUse,
230
- onDuplicate,
231
- onEdit,
232
- onDelete,
233
- }: {
234
- template: PageTemplate
235
- onUse: () => void
236
- onDuplicate: () => void
237
- onEdit: () => void
238
- onDelete: () => void
239
- }) {
240
- const cat = CATEGORIES[categoryOf(template)]
241
- const sections = sectionCountOf(template)
242
- const name = template.name ?? 'Untitled template'
243
- const updated = template.updatedAt ? new Date(template.updatedAt).toLocaleDateString() : null
244
-
245
- return (
246
- <li className="group relative flex flex-col overflow-hidden rounded-[var(--r)] border border-[var(--bdr)] bg-[var(--card)] shadow-[var(--shd)] transition-[border-color,box-shadow,transform] duration-[var(--motion-fast)] focus-within:border-[var(--acc-m)] focus-within:shadow-[var(--shd2)] hover:-translate-y-0.5 hover:border-[var(--acc-m)] hover:shadow-[var(--shd2)] motion-reduce:hover:translate-y-0">
247
- <TemplatePreview template={template} sections={sections} />
248
-
249
- <div className="flex flex-1 flex-col p-4 pb-0">
250
- <div className="flex items-start justify-between gap-3">
251
- <h2 className="text-[15px] leading-snug font-medium text-[var(--txt)]">{name}</h2>
252
- {template.builtIn && (
253
- <span className="shrink-0 rounded-full border border-[var(--bdr)] bg-[var(--bg)] px-2 py-0.5 text-[10.5px] font-medium whitespace-nowrap text-[var(--sub)]">
254
- Built-in
255
- </span>
256
- )}
257
- </div>
258
- <p className="mt-2 min-h-10 text-[12.5px] leading-relaxed text-[var(--sub)]">
259
- {template.description ?? 'No description provided.'}
260
- </p>
261
-
262
- <div className="relative mt-3 flex h-[46px] items-center justify-between border-t border-[var(--bdr)] py-2.5">
263
- <span
264
- className={`inline-flex items-center gap-1.5 text-[11.5px] font-medium ${cat.text}`}
265
- >
266
- <span aria-hidden className={`h-[7px] w-[7px] shrink-0 rounded-full ${cat.dot}`} />
267
- {cat.label}
268
- </span>
269
- <span className="text-[11.5px] text-[var(--muted)] transition-opacity duration-[var(--motion-fast)] group-focus-within:opacity-0 group-hover:opacity-0">
270
- {sections} section{sections === 1 ? '' : 's'}
271
- {updated ? ` · ${updated}` : ''}
272
- </span>
273
- <RowActions className="absolute top-1/2 right-0 -translate-y-1/2">
274
- <RowActionButton aria-label={`Create page from ${name}`} onClick={onUse}>
275
- <FilePlus size={14} aria-hidden />
276
- </RowActionButton>
277
- <RowActionButton aria-label={`Duplicate ${name}`} onClick={onDuplicate}>
278
- <Copy size={14} aria-hidden />
279
- </RowActionButton>
280
- {!template.builtIn && (
281
- <>
282
- <RowActionButton aria-label={`Edit ${name}`} onClick={onEdit}>
283
- <Pencil size={14} aria-hidden />
284
- </RowActionButton>
285
- <RowActionButton aria-label={`Delete ${name}`} danger onClick={onDelete}>
286
- <Trash2 size={14} aria-hidden />
287
- </RowActionButton>
288
- </>
289
- )}
290
- </RowActions>
291
- </div>
292
- </div>
293
- </li>
294
- )
295
- }
296
-
297
- /**
298
- * Card preview area. Uses the stored thumbnail when one exists; otherwise
299
- * renders an honest wireframe placeholder built from the template's section
300
- * count (a hero band plus one bar per section) — no fake screenshots.
301
- */
302
- function TemplatePreview({ template, sections }: { template: PageTemplate; sections: number }) {
303
- if (template.thumbnail) {
304
- return (
305
- <div className="h-28 shrink-0 overflow-hidden border-b border-[var(--bdr)] bg-[var(--bg)]">
306
- <img
307
- src={template.thumbnail}
308
- alt={`Preview of ${template.name ?? 'template'}`}
309
- className="h-full w-full object-cover"
310
- />
311
- </div>
312
- )
313
- }
314
-
315
- const bars = Math.max(0, Math.min(sections - 1, 3))
316
- return (
317
- <div
318
- aria-hidden
319
- className="flex h-28 shrink-0 flex-col gap-1.5 border-b border-[var(--bdr)] bg-[var(--bg)] p-3"
320
- >
321
- <div className="h-9 rounded-[5px] bg-[var(--acc-l)]" />
322
- {Array.from({ length: bars }, (_, i) => (
323
- <div
324
- key={i}
325
- className="h-3.5 rounded-[4px] bg-[var(--bdr)]"
326
- style={{ width: `${88 - i * 14}%` }}
327
- />
328
- ))}
329
- </div>
330
- )
331
- }
332
-
333
- function TemplateEditModal({
334
- template,
335
- onClose,
336
- onSaved,
337
- }: {
338
- template: PageTemplate | null
339
- onClose: () => void
340
- onSaved: () => void
341
- }) {
342
- // Keyed remount seeds the form state from the selected template each time
343
- // the modal opens, without effect-based state syncing.
344
- if (!template) return null
345
- return (
346
- <TemplateEditForm key={template.id} template={template} onClose={onClose} onSaved={onSaved} />
347
- )
348
- }
349
-
350
- function TemplateEditForm({
351
- template,
352
- onClose,
353
- onSaved,
354
- }: {
355
- template: PageTemplate
356
- onClose: () => void
357
- onSaved: () => void
358
- }) {
359
- const [name, setName] = useState(template.name ?? '')
360
- const [description, setDescription] = useState(template.description ?? '')
361
- const [category, setCategory] = useState<CategoryId>(categoryOf(template))
362
- const [saving, setSaving] = useState(false)
363
-
364
- async function handleSave() {
365
- if (!name.trim()) {
366
- toast.error('Template name is required')
367
- return
368
- }
369
- setSaving(true)
370
- const res = await cmsApi(`/page-templates/${template.id}`, {
371
- method: 'PUT',
372
- body: JSON.stringify({
373
- name: name.trim(),
374
- description: description.trim() || null,
375
- category,
376
- }),
377
- })
378
- setSaving(false)
379
- if (res.error) {
380
- toast.error(res.error)
381
- } else {
382
- toast.success('Template updated')
383
- onSaved()
384
- }
385
- }
386
-
387
- return (
388
- <Modal
389
- open
390
- onClose={onClose}
391
- title="Edit template"
392
- actions={
393
- <>
394
- <Button variant="ghost" onClick={onClose} disabled={saving}>
395
- Cancel
396
- </Button>
397
- <Button variant="primary" onClick={() => void handleSave()} loading={saving}>
398
- Save changes
399
- </Button>
400
- </>
401
- }
402
- >
403
- <div className="flex flex-col gap-4">
404
- <label className="flex flex-col gap-1.5">
405
- <span className="text-sm font-medium text-[var(--txt)]">Name</span>
406
- <Input
407
- value={name}
408
- onChange={(e) => setName(e.target.value)}
409
- placeholder="Template name"
410
- />
411
- </label>
412
- <label className="flex flex-col gap-1.5">
413
- <span className="text-sm font-medium text-[var(--txt)]">Description</span>
414
- <textarea
415
- value={description}
416
- onChange={(e) => setDescription(e.target.value)}
417
- rows={3}
418
- placeholder="What this template is for…"
419
- className="w-full resize-y rounded-[var(--r-control)] border border-[var(--bdr)] bg-[var(--input-background)] px-3 py-2 text-sm text-[var(--txt)] placeholder:text-[var(--muted)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
420
- />
421
- </label>
422
- <label className="flex flex-col gap-1.5">
423
- <span className="text-sm font-medium text-[var(--txt)]">Category</span>
424
- <Select value={category} onChange={(e) => setCategory(e.target.value as CategoryId)}>
425
- {(Object.keys(CATEGORIES) as CategoryId[]).map((cat) => (
426
- <option key={cat} value={cat}>
427
- {CATEGORIES[cat].label}
428
- </option>
429
- ))}
430
- </Select>
431
- </label>
432
- </div>
433
- </Modal>
434
- )
435
- }
@@ -1,113 +0,0 @@
1
- 'use client'
2
-
3
- import { useEffect, useMemo, useState } from 'react'
4
- import type { FieldDefinition } from '@prenta/core'
5
- import { cmsApi } from '../../lib/api.js'
6
-
7
- const INPUT_CLASS =
8
- 'w-full px-3 py-2 text-sm bg-background border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring'
9
- const LABEL_CLASS = 'text-sm font-medium text-foreground mb-1 block'
10
-
11
- interface RelationshipOption {
12
- id: string
13
- label: string
14
- }
15
-
16
- export interface RelationshipFieldProps {
17
- definition: FieldDefinition
18
- value: unknown
19
- onChange: (value: unknown) => void
20
- label: string
21
- }
22
-
23
- /**
24
- * Relationship picker for page-builder block fields (e.g. the form block's
25
- * `formId`). Loads the target collection via the admin API and renders a
26
- * labelled dropdown. If the collection can't be loaded it degrades to a plain
27
- * id input so the field is never a dead end.
28
- */
29
- export function RelationshipField({ definition, value, onChange, label }: RelationshipFieldProps) {
30
- const relationTo = useMemo(() => {
31
- if (definition.type !== 'relationship') return ''
32
- return Array.isArray(definition.relationTo)
33
- ? (definition.relationTo[0] ?? '')
34
- : definition.relationTo
35
- }, [definition])
36
-
37
- const [options, setOptions] = useState<RelationshipOption[]>([])
38
- const [loading, setLoading] = useState(false)
39
- const [failed, setFailed] = useState(false)
40
-
41
- useEffect(() => {
42
- if (!relationTo) return
43
- let cancelled = false
44
- setLoading(true)
45
- setFailed(false)
46
- cmsApi<Array<Record<string, unknown>>>(`/${relationTo}`)
47
- .then((res) => {
48
- if (cancelled) return
49
- if (Array.isArray(res.data)) {
50
- setOptions(
51
- res.data
52
- .map((item) => ({
53
- id: String(item.id ?? ''),
54
- label: String(item.name ?? item.title ?? item.slug ?? item.id ?? 'Untitled'),
55
- }))
56
- .filter((option) => option.id),
57
- )
58
- } else {
59
- setFailed(true)
60
- }
61
- })
62
- .catch(() => {
63
- if (!cancelled) setFailed(true)
64
- })
65
- .finally(() => {
66
- if (!cancelled) setLoading(false)
67
- })
68
- return () => {
69
- cancelled = true
70
- }
71
- }, [relationTo])
72
-
73
- const current = (value as string) ?? ''
74
- const noun = relationTo === 'forms' ? 'a form' : 'an item'
75
-
76
- if (failed) {
77
- return (
78
- <div>
79
- <label className={LABEL_CLASS}>{label}</label>
80
- <input
81
- type="text"
82
- value={current}
83
- onChange={(e) => onChange(e.target.value || undefined)}
84
- placeholder={`Enter ${relationTo} id…`}
85
- className={INPUT_CLASS}
86
- />
87
- </div>
88
- )
89
- }
90
-
91
- return (
92
- <div>
93
- <label className={LABEL_CLASS}>{label}</label>
94
- <select
95
- value={current}
96
- onChange={(e) => onChange(e.target.value || undefined)}
97
- disabled={loading}
98
- aria-label={label}
99
- className={INPUT_CLASS}
100
- >
101
- <option value="">{loading ? 'Loading…' : `Select ${noun}…`}</option>
102
- {options.map((option) => (
103
- <option key={option.id} value={option.id}>
104
- {option.label}
105
- </option>
106
- ))}
107
- </select>
108
- {!loading && options.length === 0 && (
109
- <p className="text-muted-foreground mt-1 text-xs">No {relationTo} found.</p>
110
- )}
111
- </div>
112
- )
113
- }