@actuate-media/cms-admin 0.6.0 → 0.7.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.
Files changed (215) hide show
  1. package/dist/AdminRoot.d.ts.map +1 -1
  2. package/dist/AdminRoot.js +17 -0
  3. package/dist/AdminRoot.js.map +1 -1
  4. package/dist/actuate-admin.css +1 -1
  5. package/dist/components/ErrorBoundary.js +1 -1
  6. package/dist/components/ErrorBoundary.js.map +1 -1
  7. package/dist/hooks/useBuilderState.d.ts +49 -0
  8. package/dist/hooks/useBuilderState.d.ts.map +1 -0
  9. package/dist/hooks/useBuilderState.js +238 -0
  10. package/dist/hooks/useBuilderState.js.map +1 -0
  11. package/dist/index.d.ts +3 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/layout/Sidebar.d.ts.map +1 -1
  16. package/dist/layout/Sidebar.js +2 -2
  17. package/dist/layout/Sidebar.js.map +1 -1
  18. package/dist/views/FormSubmissions.js +11 -11
  19. package/dist/views/FormSubmissions.js.map +1 -1
  20. package/dist/views/Forms.js +1 -1
  21. package/dist/views/Forms.js.map +1 -1
  22. package/dist/views/MediaBrowser.d.ts.map +1 -1
  23. package/dist/views/MediaBrowser.js +28 -8
  24. package/dist/views/MediaBrowser.js.map +1 -1
  25. package/dist/views/Posts.js +1 -1
  26. package/dist/views/Posts.js.map +1 -1
  27. package/dist/views/Redirects.js +2 -2
  28. package/dist/views/Redirects.js.map +1 -1
  29. package/dist/views/SEO.js +3 -3
  30. package/dist/views/SEO.js.map +1 -1
  31. package/dist/views/Users.js +3 -3
  32. package/dist/views/Users.js.map +1 -1
  33. package/dist/views/page-builder/AIBlockAssist.d.ts +9 -0
  34. package/dist/views/page-builder/AIBlockAssist.d.ts.map +1 -0
  35. package/dist/views/page-builder/AIBlockAssist.js +40 -0
  36. package/dist/views/page-builder/AIBlockAssist.js.map +1 -0
  37. package/dist/views/page-builder/AIGenerateDialog.d.ts +8 -0
  38. package/dist/views/page-builder/AIGenerateDialog.d.ts.map +1 -0
  39. package/dist/views/page-builder/AIGenerateDialog.js +170 -0
  40. package/dist/views/page-builder/AIGenerateDialog.js.map +1 -0
  41. package/dist/views/page-builder/BlockEditor.d.ts +11 -0
  42. package/dist/views/page-builder/BlockEditor.d.ts.map +1 -0
  43. package/dist/views/page-builder/BlockEditor.js +67 -0
  44. package/dist/views/page-builder/BlockEditor.js.map +1 -0
  45. package/dist/views/page-builder/BlockPicker.d.ts +7 -0
  46. package/dist/views/page-builder/BlockPicker.d.ts.map +1 -0
  47. package/dist/views/page-builder/BlockPicker.js +102 -0
  48. package/dist/views/page-builder/BlockPicker.js.map +1 -0
  49. package/dist/views/page-builder/BottomBar.d.ts +9 -0
  50. package/dist/views/page-builder/BottomBar.d.ts.map +1 -0
  51. package/dist/views/page-builder/BottomBar.js +13 -0
  52. package/dist/views/page-builder/BottomBar.js.map +1 -0
  53. package/dist/views/page-builder/BuilderToolbar.d.ts +21 -0
  54. package/dist/views/page-builder/BuilderToolbar.d.ts.map +1 -0
  55. package/dist/views/page-builder/BuilderToolbar.js +18 -0
  56. package/dist/views/page-builder/BuilderToolbar.js.map +1 -0
  57. package/dist/views/page-builder/ContextPanel.d.ts +20 -0
  58. package/dist/views/page-builder/ContextPanel.d.ts.map +1 -0
  59. package/dist/views/page-builder/ContextPanel.js +40 -0
  60. package/dist/views/page-builder/ContextPanel.js.map +1 -0
  61. package/dist/views/page-builder/DesignScore.d.ts +6 -0
  62. package/dist/views/page-builder/DesignScore.d.ts.map +1 -0
  63. package/dist/views/page-builder/DesignScore.js +93 -0
  64. package/dist/views/page-builder/DesignScore.js.map +1 -0
  65. package/dist/views/page-builder/NodeSettings.d.ts +12 -0
  66. package/dist/views/page-builder/NodeSettings.d.ts.map +1 -0
  67. package/dist/views/page-builder/NodeSettings.js +80 -0
  68. package/dist/views/page-builder/NodeSettings.js.map +1 -0
  69. package/dist/views/page-builder/PageBuilder.d.ts +8 -0
  70. package/dist/views/page-builder/PageBuilder.d.ts.map +1 -0
  71. package/dist/views/page-builder/PageBuilder.js +126 -0
  72. package/dist/views/page-builder/PageBuilder.js.map +1 -0
  73. package/dist/views/page-builder/PageSettings.d.ts +7 -0
  74. package/dist/views/page-builder/PageSettings.d.ts.map +1 -0
  75. package/dist/views/page-builder/PageSettings.js +27 -0
  76. package/dist/views/page-builder/PageSettings.js.map +1 -0
  77. package/dist/views/page-builder/PageTemplates.d.ts +5 -0
  78. package/dist/views/page-builder/PageTemplates.d.ts.map +1 -0
  79. package/dist/views/page-builder/PageTemplates.js +13 -0
  80. package/dist/views/page-builder/PageTemplates.js.map +1 -0
  81. package/dist/views/page-builder/SEOPanel.d.ts +10 -0
  82. package/dist/views/page-builder/SEOPanel.d.ts.map +1 -0
  83. package/dist/views/page-builder/SEOPanel.js +105 -0
  84. package/dist/views/page-builder/SEOPanel.js.map +1 -0
  85. package/dist/views/page-builder/SavedSections.d.ts +6 -0
  86. package/dist/views/page-builder/SavedSections.d.ts.map +1 -0
  87. package/dist/views/page-builder/SavedSections.js +145 -0
  88. package/dist/views/page-builder/SavedSections.js.map +1 -0
  89. package/dist/views/page-builder/TemplatePicker.d.ts +7 -0
  90. package/dist/views/page-builder/TemplatePicker.d.ts.map +1 -0
  91. package/dist/views/page-builder/TemplatePicker.js +68 -0
  92. package/dist/views/page-builder/TemplatePicker.js.map +1 -0
  93. package/dist/views/page-builder/block-renderers/CTAPreview.d.ts +3 -0
  94. package/dist/views/page-builder/block-renderers/CTAPreview.d.ts.map +1 -0
  95. package/dist/views/page-builder/block-renderers/CTAPreview.js +19 -0
  96. package/dist/views/page-builder/block-renderers/CTAPreview.js.map +1 -0
  97. package/dist/views/page-builder/block-renderers/CardsPreview.d.ts +3 -0
  98. package/dist/views/page-builder/block-renderers/CardsPreview.d.ts.map +1 -0
  99. package/dist/views/page-builder/block-renderers/CardsPreview.js +22 -0
  100. package/dist/views/page-builder/block-renderers/CardsPreview.js.map +1 -0
  101. package/dist/views/page-builder/block-renderers/CodePreview.d.ts +3 -0
  102. package/dist/views/page-builder/block-renderers/CodePreview.d.ts.map +1 -0
  103. package/dist/views/page-builder/block-renderers/CodePreview.js +16 -0
  104. package/dist/views/page-builder/block-renderers/CodePreview.js.map +1 -0
  105. package/dist/views/page-builder/block-renderers/FAQPreview.d.ts +3 -0
  106. package/dist/views/page-builder/block-renderers/FAQPreview.d.ts.map +1 -0
  107. package/dist/views/page-builder/block-renderers/FAQPreview.js +24 -0
  108. package/dist/views/page-builder/block-renderers/FAQPreview.js.map +1 -0
  109. package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts +6 -0
  110. package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts.map +1 -0
  111. package/dist/views/page-builder/block-renderers/FallbackPreview.js +7 -0
  112. package/dist/views/page-builder/block-renderers/FallbackPreview.js.map +1 -0
  113. package/dist/views/page-builder/block-renderers/FormPreview.d.ts +3 -0
  114. package/dist/views/page-builder/block-renderers/FormPreview.d.ts.map +1 -0
  115. package/dist/views/page-builder/block-renderers/FormPreview.js +14 -0
  116. package/dist/views/page-builder/block-renderers/FormPreview.js.map +1 -0
  117. package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts +3 -0
  118. package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts.map +1 -0
  119. package/dist/views/page-builder/block-renderers/GalleryPreview.js +21 -0
  120. package/dist/views/page-builder/block-renderers/GalleryPreview.js.map +1 -0
  121. package/dist/views/page-builder/block-renderers/HeroPreview.d.ts +3 -0
  122. package/dist/views/page-builder/block-renderers/HeroPreview.d.ts.map +1 -0
  123. package/dist/views/page-builder/block-renderers/HeroPreview.js +19 -0
  124. package/dist/views/page-builder/block-renderers/HeroPreview.js.map +1 -0
  125. package/dist/views/page-builder/block-renderers/ImagePreview.d.ts +3 -0
  126. package/dist/views/page-builder/block-renderers/ImagePreview.d.ts.map +1 -0
  127. package/dist/views/page-builder/block-renderers/ImagePreview.js +17 -0
  128. package/dist/views/page-builder/block-renderers/ImagePreview.js.map +1 -0
  129. package/dist/views/page-builder/block-renderers/TextPreview.d.ts +3 -0
  130. package/dist/views/page-builder/block-renderers/TextPreview.d.ts.map +1 -0
  131. package/dist/views/page-builder/block-renderers/TextPreview.js +26 -0
  132. package/dist/views/page-builder/block-renderers/TextPreview.js.map +1 -0
  133. package/dist/views/page-builder/block-renderers/VideoPreview.d.ts +3 -0
  134. package/dist/views/page-builder/block-renderers/VideoPreview.d.ts.map +1 -0
  135. package/dist/views/page-builder/block-renderers/VideoPreview.js +21 -0
  136. package/dist/views/page-builder/block-renderers/VideoPreview.js.map +1 -0
  137. package/dist/views/page-builder/block-renderers/index.d.ts +9 -0
  138. package/dist/views/page-builder/block-renderers/index.d.ts.map +1 -0
  139. package/dist/views/page-builder/block-renderers/index.js +25 -0
  140. package/dist/views/page-builder/block-renderers/index.js.map +1 -0
  141. package/dist/views/page-builder/canvas/BlockRenderer.d.ts +8 -0
  142. package/dist/views/page-builder/canvas/BlockRenderer.d.ts.map +1 -0
  143. package/dist/views/page-builder/canvas/BlockRenderer.js +30 -0
  144. package/dist/views/page-builder/canvas/BlockRenderer.js.map +1 -0
  145. package/dist/views/page-builder/canvas/BuilderCanvas.d.ts +10 -0
  146. package/dist/views/page-builder/canvas/BuilderCanvas.d.ts.map +1 -0
  147. package/dist/views/page-builder/canvas/BuilderCanvas.js +26 -0
  148. package/dist/views/page-builder/canvas/BuilderCanvas.js.map +1 -0
  149. package/dist/views/page-builder/canvas/ColumnRenderer.d.ts +8 -0
  150. package/dist/views/page-builder/canvas/ColumnRenderer.d.ts.map +1 -0
  151. package/dist/views/page-builder/canvas/ColumnRenderer.js +36 -0
  152. package/dist/views/page-builder/canvas/ColumnRenderer.js.map +1 -0
  153. package/dist/views/page-builder/canvas/ContainerRenderer.d.ts +8 -0
  154. package/dist/views/page-builder/canvas/ContainerRenderer.d.ts.map +1 -0
  155. package/dist/views/page-builder/canvas/ContainerRenderer.js +33 -0
  156. package/dist/views/page-builder/canvas/ContainerRenderer.js.map +1 -0
  157. package/dist/views/page-builder/canvas/RowRenderer.d.ts +8 -0
  158. package/dist/views/page-builder/canvas/RowRenderer.d.ts.map +1 -0
  159. package/dist/views/page-builder/canvas/RowRenderer.js +32 -0
  160. package/dist/views/page-builder/canvas/RowRenderer.js.map +1 -0
  161. package/dist/views/page-builder/canvas/SectionRenderer.d.ts +8 -0
  162. package/dist/views/page-builder/canvas/SectionRenderer.d.ts.map +1 -0
  163. package/dist/views/page-builder/canvas/SectionRenderer.js +54 -0
  164. package/dist/views/page-builder/canvas/SectionRenderer.js.map +1 -0
  165. package/dist/views/page-builder/canvas/index.d.ts +3 -0
  166. package/dist/views/page-builder/canvas/index.d.ts.map +1 -0
  167. package/dist/views/page-builder/canvas/index.js +2 -0
  168. package/dist/views/page-builder/canvas/index.js.map +1 -0
  169. package/package.json +3 -2
  170. package/src/AdminRoot.tsx +21 -0
  171. package/src/components/ErrorBoundary.tsx +3 -3
  172. package/src/hooks/useBuilderState.ts +328 -0
  173. package/src/index.ts +4 -0
  174. package/src/layout/Sidebar.tsx +5 -0
  175. package/src/views/FormSubmissions.tsx +12 -12
  176. package/src/views/Forms.tsx +1 -1
  177. package/src/views/MediaBrowser.tsx +46 -15
  178. package/src/views/Posts.tsx +1 -1
  179. package/src/views/Redirects.tsx +2 -2
  180. package/src/views/SEO.tsx +3 -3
  181. package/src/views/Users.tsx +3 -3
  182. package/src/views/page-builder/AIBlockAssist.tsx +68 -0
  183. package/src/views/page-builder/AIGenerateDialog.tsx +574 -0
  184. package/src/views/page-builder/BlockEditor.tsx +352 -0
  185. package/src/views/page-builder/BlockPicker.tsx +338 -0
  186. package/src/views/page-builder/BottomBar.tsx +64 -0
  187. package/src/views/page-builder/BuilderToolbar.tsx +218 -0
  188. package/src/views/page-builder/ContextPanel.tsx +145 -0
  189. package/src/views/page-builder/DesignScore.tsx +258 -0
  190. package/src/views/page-builder/NodeSettings.tsx +515 -0
  191. package/src/views/page-builder/PageBuilder.tsx +288 -0
  192. package/src/views/page-builder/PageSettings.tsx +161 -0
  193. package/src/views/page-builder/PageTemplates.tsx +105 -0
  194. package/src/views/page-builder/SEOPanel.tsx +485 -0
  195. package/src/views/page-builder/SavedSections.tsx +486 -0
  196. package/src/views/page-builder/TemplatePicker.tsx +201 -0
  197. package/src/views/page-builder/block-renderers/CTAPreview.tsx +81 -0
  198. package/src/views/page-builder/block-renderers/CardsPreview.tsx +71 -0
  199. package/src/views/page-builder/block-renderers/CodePreview.tsx +46 -0
  200. package/src/views/page-builder/block-renderers/FAQPreview.tsx +90 -0
  201. package/src/views/page-builder/block-renderers/FallbackPreview.tsx +18 -0
  202. package/src/views/page-builder/block-renderers/FormPreview.tsx +69 -0
  203. package/src/views/page-builder/block-renderers/GalleryPreview.tsx +93 -0
  204. package/src/views/page-builder/block-renderers/HeroPreview.tsx +103 -0
  205. package/src/views/page-builder/block-renderers/ImagePreview.tsx +54 -0
  206. package/src/views/page-builder/block-renderers/TextPreview.tsx +81 -0
  207. package/src/views/page-builder/block-renderers/VideoPreview.tsx +78 -0
  208. package/src/views/page-builder/block-renderers/index.ts +34 -0
  209. package/src/views/page-builder/canvas/BlockRenderer.tsx +62 -0
  210. package/src/views/page-builder/canvas/BuilderCanvas.tsx +90 -0
  211. package/src/views/page-builder/canvas/ColumnRenderer.tsx +86 -0
  212. package/src/views/page-builder/canvas/ContainerRenderer.tsx +71 -0
  213. package/src/views/page-builder/canvas/RowRenderer.tsx +72 -0
  214. package/src/views/page-builder/canvas/SectionRenderer.tsx +97 -0
  215. package/src/views/page-builder/canvas/index.ts +2 -0
@@ -0,0 +1,486 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import {
5
+ Plus,
6
+ ArrowLeft,
7
+ Edit,
8
+ Trash2,
9
+ Copy,
10
+ Layers,
11
+ Loader2,
12
+ AlertTriangle,
13
+ Calendar,
14
+ Hash,
15
+ } from 'lucide-react';
16
+ import { toast } from 'sonner';
17
+ import { cmsApi } from '../../lib/api.js';
18
+ import { createSection, createContainer, createRow, createColumn } from '@actuate-media/cms-core';
19
+
20
+ export interface SavedSectionsProps {
21
+ onNavigate: (path: string) => void;
22
+ config: any;
23
+ }
24
+
25
+ interface SavedSection {
26
+ id: string;
27
+ name: string;
28
+ description: string;
29
+ category: string;
30
+ tree: Record<string, unknown> | null;
31
+ usageCount: number;
32
+ createdAt: string;
33
+ updatedAt: string;
34
+ }
35
+
36
+ type ViewState =
37
+ | { mode: 'list' }
38
+ | { mode: 'create' }
39
+ | { mode: 'edit'; section: SavedSection };
40
+
41
+ const CATEGORIES = ['header', 'footer', 'content', 'sidebar'] as const;
42
+ type Category = (typeof CATEGORIES)[number];
43
+
44
+ const CATEGORY_COLORS: Record<string, string> = {
45
+ header: 'bg-primary/10 text-primary',
46
+ footer: 'bg-accent text-foreground',
47
+ content: 'bg-muted text-muted-foreground',
48
+ sidebar: 'bg-primary/10 text-primary',
49
+ };
50
+
51
+ function formatDate(dateStr: string): string {
52
+ return new Date(dateStr).toLocaleDateString(undefined, {
53
+ month: 'short',
54
+ day: 'numeric',
55
+ year: 'numeric',
56
+ });
57
+ }
58
+
59
+ function createEmptySectionTree(): Record<string, unknown> {
60
+ const col = createColumn(100);
61
+ const row = createRow([col]);
62
+ const container = createContainer();
63
+ (container as unknown as { children: unknown[] }).children = [row];
64
+ const section = createSection();
65
+ (section as unknown as { children: unknown[] }).children = [container];
66
+ return section as unknown as Record<string, unknown>;
67
+ }
68
+
69
+ export function SavedSections({ onNavigate, config }: SavedSectionsProps) {
70
+ const [sections, setSections] = useState<SavedSection[]>([]);
71
+ const [loading, setLoading] = useState(true);
72
+ const [error, setError] = useState<string | null>(null);
73
+ const [viewState, setViewState] = useState<ViewState>({ mode: 'list' });
74
+ const [activeCategory, setActiveCategory] = useState<Category | 'all'>('all');
75
+ const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
76
+ const [saving, setSaving] = useState(false);
77
+
78
+ const fetchSections = useCallback(async () => {
79
+ setLoading(true);
80
+ setError(null);
81
+ const res = await cmsApi<SavedSection[]>('/saved-sections');
82
+ if (res.error) {
83
+ setError(res.error);
84
+ } else {
85
+ setSections(res.data ?? []);
86
+ }
87
+ setLoading(false);
88
+ }, []);
89
+
90
+ useEffect(() => {
91
+ fetchSections();
92
+ }, [fetchSections]);
93
+
94
+ const filteredSections =
95
+ activeCategory === 'all'
96
+ ? sections
97
+ : sections.filter((s) => s.category === activeCategory);
98
+
99
+ async function handleDelete(id: string) {
100
+ const res = await cmsApi(`/saved-sections/${id}`, { method: 'DELETE' });
101
+ if (res.error) {
102
+ toast.error(res.error);
103
+ } else {
104
+ toast.success('Section deleted');
105
+ setSections((prev) => prev.filter((s) => s.id !== id));
106
+ }
107
+ setDeleteConfirmId(null);
108
+ }
109
+
110
+ async function handleDuplicate(section: SavedSection) {
111
+ const res = await cmsApi<SavedSection>('/saved-sections', {
112
+ method: 'POST',
113
+ body: JSON.stringify({
114
+ name: `${section.name} (Copy)`,
115
+ description: section.description,
116
+ category: section.category,
117
+ tree: section.tree,
118
+ }),
119
+ });
120
+ if (res.error) {
121
+ toast.error(res.error);
122
+ } else {
123
+ toast.success('Section duplicated');
124
+ fetchSections();
125
+ }
126
+ }
127
+
128
+ if (viewState.mode === 'create' || viewState.mode === 'edit') {
129
+ return (
130
+ <SectionForm
131
+ initial={viewState.mode === 'edit' ? viewState.section : undefined}
132
+ saving={saving}
133
+ onSave={async (data) => {
134
+ setSaving(true);
135
+ if (viewState.mode === 'create') {
136
+ const res = await cmsApi<SavedSection>('/saved-sections', {
137
+ method: 'POST',
138
+ body: JSON.stringify({ ...data, tree: createEmptySectionTree() }),
139
+ });
140
+ setSaving(false);
141
+ if (res.error) {
142
+ toast.error(res.error);
143
+ } else {
144
+ toast.success('Section created');
145
+ setViewState({ mode: 'list' });
146
+ fetchSections();
147
+ }
148
+ } else {
149
+ const res = await cmsApi<SavedSection>(
150
+ `/saved-sections/${viewState.section.id}`,
151
+ {
152
+ method: 'PUT',
153
+ body: JSON.stringify(data),
154
+ }
155
+ );
156
+ setSaving(false);
157
+ if (res.error) {
158
+ toast.error(res.error);
159
+ } else {
160
+ toast.success('Section updated');
161
+ setViewState({ mode: 'list' });
162
+ fetchSections();
163
+ }
164
+ }
165
+ }}
166
+ onCancel={() => setViewState({ mode: 'list' })}
167
+ />
168
+ );
169
+ }
170
+
171
+ return (
172
+ <div className="p-4 pr-8">
173
+ <div className="mb-6 flex items-center justify-between">
174
+ <div>
175
+ <h1 className="text-2xl font-medium text-foreground">Saved Sections</h1>
176
+ <p className="text-sm text-muted-foreground mt-1">
177
+ Reusable sections for the page builder
178
+ </p>
179
+ </div>
180
+ <button
181
+ type="button"
182
+ onClick={() => setViewState({ mode: 'create' })}
183
+ className="flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
184
+ >
185
+ <Plus size={16} />
186
+ Create Section
187
+ </button>
188
+ </div>
189
+
190
+ <div className="mb-4 flex items-center gap-1 border-b border-border">
191
+ <CategoryTab
192
+ label="All"
193
+ active={activeCategory === 'all'}
194
+ onClick={() => setActiveCategory('all')}
195
+ />
196
+ {CATEGORIES.map((cat) => (
197
+ <CategoryTab
198
+ key={cat}
199
+ label={cat.charAt(0).toUpperCase() + cat.slice(1)}
200
+ active={activeCategory === cat}
201
+ onClick={() => setActiveCategory(cat)}
202
+ />
203
+ ))}
204
+ </div>
205
+
206
+ {loading && (
207
+ <div className="flex items-center justify-center py-16">
208
+ <Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
209
+ </div>
210
+ )}
211
+
212
+ {error && !loading && (
213
+ <div className="flex flex-col items-center justify-center py-12 text-center">
214
+ <AlertTriangle size={24} className="text-destructive mb-2" />
215
+ <p className="text-sm text-muted-foreground mb-3">{error}</p>
216
+ <button
217
+ type="button"
218
+ onClick={fetchSections}
219
+ className="px-3 py-1.5 text-sm font-medium text-foreground border border-border rounded-md hover:bg-accent transition-colors"
220
+ >
221
+ Retry
222
+ </button>
223
+ </div>
224
+ )}
225
+
226
+ {!loading && !error && filteredSections.length === 0 && (
227
+ <div className="flex flex-col items-center justify-center py-16 text-center">
228
+ <Layers size={32} className="text-muted-foreground mb-3" />
229
+ <p className="text-sm font-medium text-foreground mb-1">No saved sections</p>
230
+ <p className="text-xs text-muted-foreground mb-4">
231
+ {activeCategory === 'all'
232
+ ? 'Create your first reusable section to get started.'
233
+ : `No sections in the "${activeCategory}" category.`}
234
+ </p>
235
+ {activeCategory === 'all' && (
236
+ <button
237
+ type="button"
238
+ onClick={() => setViewState({ mode: 'create' })}
239
+ className="flex items-center gap-2 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
240
+ >
241
+ <Plus size={14} />
242
+ Create Section
243
+ </button>
244
+ )}
245
+ </div>
246
+ )}
247
+
248
+ {!loading && !error && filteredSections.length > 0 && (
249
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
250
+ {filteredSections.map((section) => (
251
+ <div
252
+ key={section.id}
253
+ className="border border-border rounded-lg p-4 bg-card hover:border-primary/50 transition-colors"
254
+ >
255
+ <div className="flex items-start justify-between gap-3">
256
+ <div className="min-w-0 flex-1">
257
+ <p className="text-sm font-medium text-foreground truncate">
258
+ {section.name}
259
+ </p>
260
+ {section.description && (
261
+ <p className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
262
+ {section.description}
263
+ </p>
264
+ )}
265
+ </div>
266
+ <span
267
+ className={`shrink-0 text-xs px-2 py-0.5 rounded-full ${CATEGORY_COLORS[section.category] ?? 'bg-muted text-muted-foreground'}`}
268
+ >
269
+ {section.category}
270
+ </span>
271
+ </div>
272
+
273
+ <div className="mt-3 flex items-center gap-4 text-xs text-muted-foreground">
274
+ <span className="flex items-center gap-1">
275
+ <Hash size={12} />
276
+ {section.usageCount} use{section.usageCount !== 1 ? 's' : ''}
277
+ </span>
278
+ <span className="flex items-center gap-1">
279
+ <Calendar size={12} />
280
+ {formatDate(section.createdAt)}
281
+ </span>
282
+ {section.tree && (
283
+ <span className="flex items-center gap-1">
284
+ <Layers size={12} />
285
+ {JSON.stringify(section.tree).length > 1000 ? 'Complex' : 'Simple'}
286
+ </span>
287
+ )}
288
+ </div>
289
+
290
+ <div className="mt-3 flex items-center gap-1 border-t border-border pt-3">
291
+ <button
292
+ type="button"
293
+ onClick={() => setViewState({ mode: 'edit', section })}
294
+ className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-foreground rounded-md hover:bg-accent transition-colors"
295
+ aria-label={`Edit ${section.name}`}
296
+ >
297
+ <Edit size={13} />
298
+ Edit
299
+ </button>
300
+ <button
301
+ type="button"
302
+ onClick={() => handleDuplicate(section)}
303
+ className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-foreground rounded-md hover:bg-accent transition-colors"
304
+ aria-label={`Duplicate ${section.name}`}
305
+ >
306
+ <Copy size={13} />
307
+ Duplicate
308
+ </button>
309
+ {deleteConfirmId === section.id ? (
310
+ <div className="ml-auto flex items-center gap-1">
311
+ <span className="text-xs text-destructive mr-1">Delete?</span>
312
+ <button
313
+ type="button"
314
+ onClick={() => handleDelete(section.id)}
315
+ className="px-2 py-1 text-xs font-medium text-destructive-foreground bg-destructive rounded-md hover:bg-destructive/90 transition-colors"
316
+ >
317
+ Yes
318
+ </button>
319
+ <button
320
+ type="button"
321
+ onClick={() => setDeleteConfirmId(null)}
322
+ className="px-2 py-1 text-xs font-medium text-foreground border border-border rounded-md hover:bg-accent transition-colors"
323
+ >
324
+ No
325
+ </button>
326
+ </div>
327
+ ) : (
328
+ <button
329
+ type="button"
330
+ onClick={() => setDeleteConfirmId(section.id)}
331
+ className="ml-auto flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-destructive rounded-md hover:bg-destructive/10 transition-colors"
332
+ aria-label={`Delete ${section.name}`}
333
+ >
334
+ <Trash2 size={13} />
335
+ Delete
336
+ </button>
337
+ )}
338
+ </div>
339
+ </div>
340
+ ))}
341
+ </div>
342
+ )}
343
+ </div>
344
+ );
345
+ }
346
+
347
+ interface CategoryTabProps {
348
+ label: string;
349
+ active: boolean;
350
+ onClick: () => void;
351
+ }
352
+
353
+ function CategoryTab({ label, active, onClick }: CategoryTabProps) {
354
+ return (
355
+ <button
356
+ type="button"
357
+ onClick={onClick}
358
+ className={`px-3 py-2 text-sm transition-colors border-b-2 -mb-px ${
359
+ active
360
+ ? 'border-primary text-foreground font-medium'
361
+ : 'border-transparent text-muted-foreground hover:text-foreground'
362
+ }`}
363
+ >
364
+ {label}
365
+ </button>
366
+ );
367
+ }
368
+
369
+ interface SectionFormProps {
370
+ initial?: SavedSection;
371
+ saving: boolean;
372
+ onSave: (data: { name: string; description: string; category: string }) => void;
373
+ onCancel: () => void;
374
+ }
375
+
376
+ function SectionForm({ initial, saving, onSave, onCancel }: SectionFormProps) {
377
+ const [name, setName] = useState(initial?.name ?? '');
378
+ const [description, setDescription] = useState(initial?.description ?? '');
379
+ const [category, setCategory] = useState(initial?.category ?? 'content');
380
+
381
+ function handleSubmit(e: React.FormEvent) {
382
+ e.preventDefault();
383
+ if (!name.trim()) return;
384
+ onSave({ name: name.trim(), description: description.trim(), category });
385
+ }
386
+
387
+ const treeSize = initial?.tree
388
+ ? JSON.stringify(initial.tree).length
389
+ : null;
390
+
391
+ return (
392
+ <div className="p-4 pr-8">
393
+ <div className="mb-6">
394
+ <button
395
+ type="button"
396
+ onClick={onCancel}
397
+ className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors mb-3"
398
+ aria-label="Back to saved sections"
399
+ >
400
+ <ArrowLeft size={16} />
401
+ Back
402
+ </button>
403
+ <h1 className="text-2xl font-medium text-foreground">
404
+ {initial ? `Edit: ${initial.name}` : 'Create Saved Section'}
405
+ </h1>
406
+ </div>
407
+
408
+ <form onSubmit={handleSubmit} className="max-w-lg space-y-4">
409
+ <div>
410
+ <label htmlFor="section-name" className="block text-sm font-medium text-foreground mb-1">
411
+ Name <span className="text-destructive">*</span>
412
+ </label>
413
+ <input
414
+ id="section-name"
415
+ type="text"
416
+ value={name}
417
+ onChange={(e) => setName(e.target.value)}
418
+ required
419
+ placeholder="e.g. Hero Banner"
420
+ className="w-full px-3 py-2 text-sm bg-background border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring"
421
+ />
422
+ </div>
423
+
424
+ <div>
425
+ <label htmlFor="section-description" className="block text-sm font-medium text-foreground mb-1">
426
+ Description
427
+ </label>
428
+ <textarea
429
+ id="section-description"
430
+ value={description}
431
+ onChange={(e) => setDescription(e.target.value)}
432
+ placeholder="Optional description of this section..."
433
+ rows={3}
434
+ className="w-full px-3 py-2 text-sm bg-background border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring resize-none"
435
+ />
436
+ </div>
437
+
438
+ <div>
439
+ <label htmlFor="section-category" className="block text-sm font-medium text-foreground mb-1">
440
+ Category
441
+ </label>
442
+ <select
443
+ id="section-category"
444
+ value={category}
445
+ onChange={(e) => setCategory(e.target.value)}
446
+ className="w-full px-3 py-2 text-sm bg-background border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring"
447
+ >
448
+ {CATEGORIES.map((cat) => (
449
+ <option key={cat} value={cat}>
450
+ {cat.charAt(0).toUpperCase() + cat.slice(1)}
451
+ </option>
452
+ ))}
453
+ </select>
454
+ </div>
455
+
456
+ {treeSize !== null && (
457
+ <div className="rounded-md border border-border bg-muted p-3">
458
+ <p className="text-xs text-muted-foreground">
459
+ Section tree editing is available in the page builder.
460
+ Current tree size: {(treeSize / 1024).toFixed(1)} KB
461
+ </p>
462
+ </div>
463
+ )}
464
+
465
+ <div className="flex items-center gap-3 pt-2">
466
+ <button
467
+ type="submit"
468
+ disabled={saving || !name.trim()}
469
+ className="flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed"
470
+ >
471
+ {saving && <Loader2 size={14} className="animate-spin" />}
472
+ {initial ? 'Save Changes' : 'Create Section'}
473
+ </button>
474
+ <button
475
+ type="button"
476
+ onClick={onCancel}
477
+ disabled={saving}
478
+ className="px-4 py-2 text-sm font-medium text-foreground border border-border rounded-lg hover:bg-accent transition-colors disabled:opacity-50"
479
+ >
480
+ Cancel
481
+ </button>
482
+ </div>
483
+ </form>
484
+ </div>
485
+ );
486
+ }
@@ -0,0 +1,201 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import {
5
+ X,
6
+ Loader2,
7
+ AlertTriangle,
8
+ Layout,
9
+ FileText,
10
+ MapPin,
11
+ Phone,
12
+ BookOpen,
13
+ File,
14
+ } from 'lucide-react';
15
+ import type { LucideIcon } from 'lucide-react';
16
+ import { cmsApi } from '../../lib/api.js';
17
+ import { createEmptyPage } from '@actuate-media/cms-core';
18
+ import type { PageNode } from '@actuate-media/cms-core';
19
+
20
+ export interface TemplatePickerProps {
21
+ onSelect: (templateTree: PageNode) => void;
22
+ onClose: () => void;
23
+ }
24
+
25
+ interface PageTemplate {
26
+ id: string;
27
+ name: string;
28
+ description: string;
29
+ category: string;
30
+ tree: PageNode;
31
+ }
32
+
33
+ const categoryIcons: Record<string, LucideIcon> = {
34
+ landing: Layout,
35
+ blog: FileText,
36
+ contact: MapPin,
37
+ portfolio: BookOpen,
38
+ mobile: Phone,
39
+ };
40
+
41
+ const categoryColors: Record<string, string> = {
42
+ landing: 'bg-primary/10 text-primary',
43
+ blog: 'bg-accent text-foreground',
44
+ contact: 'bg-primary/10 text-primary',
45
+ portfolio: 'bg-accent text-foreground',
46
+ mobile: 'bg-primary/10 text-primary',
47
+ };
48
+
49
+ function resolveIcon(category: string): LucideIcon {
50
+ return categoryIcons[category] ?? File;
51
+ }
52
+
53
+ function resolveBadgeColor(category: string): string {
54
+ return categoryColors[category] ?? 'bg-muted text-muted-foreground';
55
+ }
56
+
57
+ export function TemplatePicker({ onSelect, onClose }: TemplatePickerProps) {
58
+ const [templates, setTemplates] = useState<PageTemplate[]>([]);
59
+ const [loading, setLoading] = useState(true);
60
+ const [error, setError] = useState<string | null>(null);
61
+
62
+ const fetchTemplates = useCallback(async () => {
63
+ setLoading(true);
64
+ setError(null);
65
+ const res = await cmsApi<PageTemplate[]>('/page-templates');
66
+ if (res.error) {
67
+ setError(res.error);
68
+ } else {
69
+ setTemplates(res.data ?? []);
70
+ }
71
+ setLoading(false);
72
+ }, []);
73
+
74
+ useEffect(() => {
75
+ fetchTemplates();
76
+ }, [fetchTemplates]);
77
+
78
+ useEffect(() => {
79
+ function handleKeyDown(e: KeyboardEvent) {
80
+ if (e.key === 'Escape') onClose();
81
+ }
82
+ document.addEventListener('keydown', handleKeyDown);
83
+ return () => document.removeEventListener('keydown', handleKeyDown);
84
+ }, [onClose]);
85
+
86
+ function handleBlankPage() {
87
+ onSelect(createEmptyPage());
88
+ onClose();
89
+ }
90
+
91
+ function handleTemplateSelect(template: PageTemplate) {
92
+ onSelect(template.tree);
93
+ onClose();
94
+ }
95
+
96
+ return (
97
+ <div
98
+ className="fixed inset-0 z-50 flex items-center justify-center"
99
+ role="dialog"
100
+ aria-modal="true"
101
+ aria-label="Choose a template"
102
+ >
103
+ <div
104
+ className="absolute inset-0 bg-black/50 motion-safe:animate-in motion-safe:fade-in-0"
105
+ onClick={onClose}
106
+ aria-hidden="true"
107
+ />
108
+
109
+ <div className="relative z-10 w-full max-w-2xl max-h-[80vh] bg-card rounded-xl shadow-2xl border border-border flex flex-col overflow-hidden">
110
+ <div className="flex items-center justify-between px-5 pt-5 pb-3">
111
+ <h2 className="text-lg font-medium text-foreground">Choose a Template</h2>
112
+ <button
113
+ type="button"
114
+ onClick={onClose}
115
+ className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
116
+ aria-label="Close"
117
+ >
118
+ <X size={18} />
119
+ </button>
120
+ </div>
121
+
122
+ <div className="flex-1 overflow-y-auto px-5 pb-5">
123
+ {loading && (
124
+ <div className="flex items-center justify-center py-16">
125
+ <Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
126
+ </div>
127
+ )}
128
+
129
+ {error && !loading && (
130
+ <div className="flex flex-col items-center justify-center py-12 text-center">
131
+ <AlertTriangle size={24} className="text-destructive mb-2" />
132
+ <p className="text-sm text-muted-foreground mb-3">{error}</p>
133
+ <button
134
+ type="button"
135
+ onClick={fetchTemplates}
136
+ className="px-3 py-1.5 text-sm font-medium text-foreground border border-border rounded-md hover:bg-accent transition-colors"
137
+ >
138
+ Retry
139
+ </button>
140
+ </div>
141
+ )}
142
+
143
+ {!loading && !error && (
144
+ <div
145
+ className="grid grid-cols-2 md:grid-cols-3 gap-3"
146
+ role="list"
147
+ aria-label="Available templates"
148
+ >
149
+ <button
150
+ type="button"
151
+ role="listitem"
152
+ onClick={handleBlankPage}
153
+ className="p-4 border border-border rounded-lg hover:border-primary cursor-pointer transition-colors bg-card text-left flex flex-col items-center gap-2 group focus:outline-none focus:ring-2 focus:ring-ring"
154
+ >
155
+ <div className="w-10 h-10 rounded-md bg-muted flex items-center justify-center text-muted-foreground group-hover:bg-primary/10 group-hover:text-primary transition-colors">
156
+ <File size={20} />
157
+ </div>
158
+ <p className="text-sm font-medium text-foreground text-center">Blank Page</p>
159
+ <p className="text-xs text-muted-foreground text-center">
160
+ Start from scratch
161
+ </p>
162
+ </button>
163
+
164
+ {templates.map((template) => {
165
+ const Icon = resolveIcon(template.category);
166
+ const badgeColor = resolveBadgeColor(template.category);
167
+
168
+ return (
169
+ <button
170
+ key={template.id}
171
+ type="button"
172
+ role="listitem"
173
+ onClick={() => handleTemplateSelect(template)}
174
+ className="p-4 border border-border rounded-lg hover:border-primary cursor-pointer transition-colors bg-card text-left flex flex-col items-center gap-2 group focus:outline-none focus:ring-2 focus:ring-ring"
175
+ >
176
+ <div className="w-10 h-10 rounded-md bg-accent flex items-center justify-center text-foreground group-hover:bg-primary/10 group-hover:text-primary transition-colors">
177
+ <Icon size={20} />
178
+ </div>
179
+ <p className="text-sm font-medium text-foreground text-center truncate w-full">
180
+ {template.name}
181
+ </p>
182
+ {template.description && (
183
+ <p className="text-xs text-muted-foreground text-center line-clamp-2">
184
+ {template.description}
185
+ </p>
186
+ )}
187
+ <span
188
+ className={`inline-block text-xs px-2 py-0.5 rounded-full ${badgeColor}`}
189
+ >
190
+ {template.category}
191
+ </span>
192
+ </button>
193
+ );
194
+ })}
195
+ </div>
196
+ )}
197
+ </div>
198
+ </div>
199
+ </div>
200
+ );
201
+ }