@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.
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +17 -0
- package/dist/AdminRoot.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/ErrorBoundary.js +1 -1
- package/dist/components/ErrorBoundary.js.map +1 -1
- package/dist/hooks/useBuilderState.d.ts +49 -0
- package/dist/hooks/useBuilderState.d.ts.map +1 -0
- package/dist/hooks/useBuilderState.js +238 -0
- package/dist/hooks/useBuilderState.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +2 -2
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/views/FormSubmissions.js +11 -11
- package/dist/views/FormSubmissions.js.map +1 -1
- package/dist/views/Forms.js +1 -1
- package/dist/views/Forms.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +28 -8
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/Posts.js +1 -1
- package/dist/views/Posts.js.map +1 -1
- package/dist/views/Redirects.js +2 -2
- package/dist/views/Redirects.js.map +1 -1
- package/dist/views/SEO.js +3 -3
- package/dist/views/SEO.js.map +1 -1
- package/dist/views/Users.js +3 -3
- package/dist/views/Users.js.map +1 -1
- package/dist/views/page-builder/AIBlockAssist.d.ts +9 -0
- package/dist/views/page-builder/AIBlockAssist.d.ts.map +1 -0
- package/dist/views/page-builder/AIBlockAssist.js +40 -0
- package/dist/views/page-builder/AIBlockAssist.js.map +1 -0
- package/dist/views/page-builder/AIGenerateDialog.d.ts +8 -0
- package/dist/views/page-builder/AIGenerateDialog.d.ts.map +1 -0
- package/dist/views/page-builder/AIGenerateDialog.js +170 -0
- package/dist/views/page-builder/AIGenerateDialog.js.map +1 -0
- package/dist/views/page-builder/BlockEditor.d.ts +11 -0
- package/dist/views/page-builder/BlockEditor.d.ts.map +1 -0
- package/dist/views/page-builder/BlockEditor.js +67 -0
- package/dist/views/page-builder/BlockEditor.js.map +1 -0
- package/dist/views/page-builder/BlockPicker.d.ts +7 -0
- package/dist/views/page-builder/BlockPicker.d.ts.map +1 -0
- package/dist/views/page-builder/BlockPicker.js +102 -0
- package/dist/views/page-builder/BlockPicker.js.map +1 -0
- package/dist/views/page-builder/BottomBar.d.ts +9 -0
- package/dist/views/page-builder/BottomBar.d.ts.map +1 -0
- package/dist/views/page-builder/BottomBar.js +13 -0
- package/dist/views/page-builder/BottomBar.js.map +1 -0
- package/dist/views/page-builder/BuilderToolbar.d.ts +21 -0
- package/dist/views/page-builder/BuilderToolbar.d.ts.map +1 -0
- package/dist/views/page-builder/BuilderToolbar.js +18 -0
- package/dist/views/page-builder/BuilderToolbar.js.map +1 -0
- package/dist/views/page-builder/ContextPanel.d.ts +20 -0
- package/dist/views/page-builder/ContextPanel.d.ts.map +1 -0
- package/dist/views/page-builder/ContextPanel.js +40 -0
- package/dist/views/page-builder/ContextPanel.js.map +1 -0
- package/dist/views/page-builder/DesignScore.d.ts +6 -0
- package/dist/views/page-builder/DesignScore.d.ts.map +1 -0
- package/dist/views/page-builder/DesignScore.js +93 -0
- package/dist/views/page-builder/DesignScore.js.map +1 -0
- package/dist/views/page-builder/NodeSettings.d.ts +12 -0
- package/dist/views/page-builder/NodeSettings.d.ts.map +1 -0
- package/dist/views/page-builder/NodeSettings.js +80 -0
- package/dist/views/page-builder/NodeSettings.js.map +1 -0
- package/dist/views/page-builder/PageBuilder.d.ts +8 -0
- package/dist/views/page-builder/PageBuilder.d.ts.map +1 -0
- package/dist/views/page-builder/PageBuilder.js +126 -0
- package/dist/views/page-builder/PageBuilder.js.map +1 -0
- package/dist/views/page-builder/PageSettings.d.ts +7 -0
- package/dist/views/page-builder/PageSettings.d.ts.map +1 -0
- package/dist/views/page-builder/PageSettings.js +27 -0
- package/dist/views/page-builder/PageSettings.js.map +1 -0
- package/dist/views/page-builder/PageTemplates.d.ts +5 -0
- package/dist/views/page-builder/PageTemplates.d.ts.map +1 -0
- package/dist/views/page-builder/PageTemplates.js +13 -0
- package/dist/views/page-builder/PageTemplates.js.map +1 -0
- package/dist/views/page-builder/SEOPanel.d.ts +10 -0
- package/dist/views/page-builder/SEOPanel.d.ts.map +1 -0
- package/dist/views/page-builder/SEOPanel.js +105 -0
- package/dist/views/page-builder/SEOPanel.js.map +1 -0
- package/dist/views/page-builder/SavedSections.d.ts +6 -0
- package/dist/views/page-builder/SavedSections.d.ts.map +1 -0
- package/dist/views/page-builder/SavedSections.js +145 -0
- package/dist/views/page-builder/SavedSections.js.map +1 -0
- package/dist/views/page-builder/TemplatePicker.d.ts +7 -0
- package/dist/views/page-builder/TemplatePicker.d.ts.map +1 -0
- package/dist/views/page-builder/TemplatePicker.js +68 -0
- package/dist/views/page-builder/TemplatePicker.js.map +1 -0
- package/dist/views/page-builder/block-renderers/CTAPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/CTAPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/CTAPreview.js +19 -0
- package/dist/views/page-builder/block-renderers/CTAPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/CardsPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/CardsPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/CardsPreview.js +22 -0
- package/dist/views/page-builder/block-renderers/CardsPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/CodePreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/CodePreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/CodePreview.js +16 -0
- package/dist/views/page-builder/block-renderers/CodePreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/FAQPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/FAQPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/FAQPreview.js +24 -0
- package/dist/views/page-builder/block-renderers/FAQPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts +6 -0
- package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/FallbackPreview.js +7 -0
- package/dist/views/page-builder/block-renderers/FallbackPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/FormPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/FormPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/FormPreview.js +14 -0
- package/dist/views/page-builder/block-renderers/FormPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/GalleryPreview.js +21 -0
- package/dist/views/page-builder/block-renderers/GalleryPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/HeroPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/HeroPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/HeroPreview.js +19 -0
- package/dist/views/page-builder/block-renderers/HeroPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/ImagePreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/ImagePreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/ImagePreview.js +17 -0
- package/dist/views/page-builder/block-renderers/ImagePreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/TextPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/TextPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/TextPreview.js +26 -0
- package/dist/views/page-builder/block-renderers/TextPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/VideoPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/VideoPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/VideoPreview.js +21 -0
- package/dist/views/page-builder/block-renderers/VideoPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/index.d.ts +9 -0
- package/dist/views/page-builder/block-renderers/index.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/index.js +25 -0
- package/dist/views/page-builder/block-renderers/index.js.map +1 -0
- package/dist/views/page-builder/canvas/BlockRenderer.d.ts +8 -0
- package/dist/views/page-builder/canvas/BlockRenderer.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/BlockRenderer.js +30 -0
- package/dist/views/page-builder/canvas/BlockRenderer.js.map +1 -0
- package/dist/views/page-builder/canvas/BuilderCanvas.d.ts +10 -0
- package/dist/views/page-builder/canvas/BuilderCanvas.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/BuilderCanvas.js +26 -0
- package/dist/views/page-builder/canvas/BuilderCanvas.js.map +1 -0
- package/dist/views/page-builder/canvas/ColumnRenderer.d.ts +8 -0
- package/dist/views/page-builder/canvas/ColumnRenderer.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/ColumnRenderer.js +36 -0
- package/dist/views/page-builder/canvas/ColumnRenderer.js.map +1 -0
- package/dist/views/page-builder/canvas/ContainerRenderer.d.ts +8 -0
- package/dist/views/page-builder/canvas/ContainerRenderer.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/ContainerRenderer.js +33 -0
- package/dist/views/page-builder/canvas/ContainerRenderer.js.map +1 -0
- package/dist/views/page-builder/canvas/RowRenderer.d.ts +8 -0
- package/dist/views/page-builder/canvas/RowRenderer.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/RowRenderer.js +32 -0
- package/dist/views/page-builder/canvas/RowRenderer.js.map +1 -0
- package/dist/views/page-builder/canvas/SectionRenderer.d.ts +8 -0
- package/dist/views/page-builder/canvas/SectionRenderer.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/SectionRenderer.js +54 -0
- package/dist/views/page-builder/canvas/SectionRenderer.js.map +1 -0
- package/dist/views/page-builder/canvas/index.d.ts +3 -0
- package/dist/views/page-builder/canvas/index.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/index.js +2 -0
- package/dist/views/page-builder/canvas/index.js.map +1 -0
- package/package.json +3 -2
- package/src/AdminRoot.tsx +21 -0
- package/src/components/ErrorBoundary.tsx +3 -3
- package/src/hooks/useBuilderState.ts +328 -0
- package/src/index.ts +4 -0
- package/src/layout/Sidebar.tsx +5 -0
- package/src/views/FormSubmissions.tsx +12 -12
- package/src/views/Forms.tsx +1 -1
- package/src/views/MediaBrowser.tsx +46 -15
- package/src/views/Posts.tsx +1 -1
- package/src/views/Redirects.tsx +2 -2
- package/src/views/SEO.tsx +3 -3
- package/src/views/Users.tsx +3 -3
- package/src/views/page-builder/AIBlockAssist.tsx +68 -0
- package/src/views/page-builder/AIGenerateDialog.tsx +574 -0
- package/src/views/page-builder/BlockEditor.tsx +352 -0
- package/src/views/page-builder/BlockPicker.tsx +338 -0
- package/src/views/page-builder/BottomBar.tsx +64 -0
- package/src/views/page-builder/BuilderToolbar.tsx +218 -0
- package/src/views/page-builder/ContextPanel.tsx +145 -0
- package/src/views/page-builder/DesignScore.tsx +258 -0
- package/src/views/page-builder/NodeSettings.tsx +515 -0
- package/src/views/page-builder/PageBuilder.tsx +288 -0
- package/src/views/page-builder/PageSettings.tsx +161 -0
- package/src/views/page-builder/PageTemplates.tsx +105 -0
- package/src/views/page-builder/SEOPanel.tsx +485 -0
- package/src/views/page-builder/SavedSections.tsx +486 -0
- package/src/views/page-builder/TemplatePicker.tsx +201 -0
- package/src/views/page-builder/block-renderers/CTAPreview.tsx +81 -0
- package/src/views/page-builder/block-renderers/CardsPreview.tsx +71 -0
- package/src/views/page-builder/block-renderers/CodePreview.tsx +46 -0
- package/src/views/page-builder/block-renderers/FAQPreview.tsx +90 -0
- package/src/views/page-builder/block-renderers/FallbackPreview.tsx +18 -0
- package/src/views/page-builder/block-renderers/FormPreview.tsx +69 -0
- package/src/views/page-builder/block-renderers/GalleryPreview.tsx +93 -0
- package/src/views/page-builder/block-renderers/HeroPreview.tsx +103 -0
- package/src/views/page-builder/block-renderers/ImagePreview.tsx +54 -0
- package/src/views/page-builder/block-renderers/TextPreview.tsx +81 -0
- package/src/views/page-builder/block-renderers/VideoPreview.tsx +78 -0
- package/src/views/page-builder/block-renderers/index.ts +34 -0
- package/src/views/page-builder/canvas/BlockRenderer.tsx +62 -0
- package/src/views/page-builder/canvas/BuilderCanvas.tsx +90 -0
- package/src/views/page-builder/canvas/ColumnRenderer.tsx +86 -0
- package/src/views/page-builder/canvas/ContainerRenderer.tsx +71 -0
- package/src/views/page-builder/canvas/RowRenderer.tsx +72 -0
- package/src/views/page-builder/canvas/SectionRenderer.tsx +97 -0
- 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
|
+
}
|