@actuate-media/cms-admin 0.17.0 → 0.19.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 (117) hide show
  1. package/dist/__tests__/lib/page-editor-service-api.test.d.ts +2 -0
  2. package/dist/__tests__/lib/page-editor-service-api.test.d.ts.map +1 -0
  3. package/dist/__tests__/lib/page-editor-service-api.test.js +155 -0
  4. package/dist/__tests__/lib/page-editor-service-api.test.js.map +1 -0
  5. package/dist/__tests__/lib/pages-service-fetch.test.d.ts +2 -0
  6. package/dist/__tests__/lib/pages-service-fetch.test.d.ts.map +1 -0
  7. package/dist/__tests__/lib/pages-service-fetch.test.js +125 -0
  8. package/dist/__tests__/lib/pages-service-fetch.test.js.map +1 -0
  9. package/dist/__tests__/lib/post-editor-service.test.js +75 -23
  10. package/dist/__tests__/lib/post-editor-service.test.js.map +1 -1
  11. package/dist/__tests__/lib/posts-service.test.js +132 -142
  12. package/dist/__tests__/lib/posts-service.test.js.map +1 -1
  13. package/dist/__tests__/views/page-section-editor.test.d.ts +2 -0
  14. package/dist/__tests__/views/page-section-editor.test.d.ts.map +1 -0
  15. package/dist/__tests__/views/page-section-editor.test.js +56 -0
  16. package/dist/__tests__/views/page-section-editor.test.js.map +1 -0
  17. package/dist/__tests__/views/pages-list-view.test.d.ts +2 -0
  18. package/dist/__tests__/views/pages-list-view.test.d.ts.map +1 -0
  19. package/dist/__tests__/views/pages-list-view.test.js +102 -0
  20. package/dist/__tests__/views/pages-list-view.test.js.map +1 -0
  21. package/dist/__tests__/views/posts-list-view.render.test.d.ts +2 -0
  22. package/dist/__tests__/views/posts-list-view.render.test.d.ts.map +1 -0
  23. package/dist/__tests__/views/posts-list-view.render.test.js +73 -0
  24. package/dist/__tests__/views/posts-list-view.render.test.js.map +1 -0
  25. package/dist/actuate-admin.css +1 -1
  26. package/dist/index.d.ts +0 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +0 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/lib/page-editor-service.d.ts +1 -0
  31. package/dist/lib/page-editor-service.d.ts.map +1 -1
  32. package/dist/lib/page-editor-service.js +55 -0
  33. package/dist/lib/page-editor-service.js.map +1 -1
  34. package/dist/lib/pages-service.d.ts +26 -0
  35. package/dist/lib/pages-service.d.ts.map +1 -1
  36. package/dist/lib/pages-service.js +65 -38
  37. package/dist/lib/pages-service.js.map +1 -1
  38. package/dist/lib/post-editor-service.d.ts +6 -4
  39. package/dist/lib/post-editor-service.d.ts.map +1 -1
  40. package/dist/lib/post-editor-service.js +32 -37
  41. package/dist/lib/post-editor-service.js.map +1 -1
  42. package/dist/lib/posts-service.d.ts.map +1 -1
  43. package/dist/lib/posts-service.js +91 -160
  44. package/dist/lib/posts-service.js.map +1 -1
  45. package/dist/views/Pages/PagesListView.d.ts.map +1 -1
  46. package/dist/views/Pages/PagesListView.js +71 -46
  47. package/dist/views/Pages/PagesListView.js.map +1 -1
  48. package/dist/views/Posts/NewPostPicker.d.ts +5 -4
  49. package/dist/views/Posts/NewPostPicker.d.ts.map +1 -1
  50. package/dist/views/Posts/NewPostPicker.js +5 -4
  51. package/dist/views/Posts/NewPostPicker.js.map +1 -1
  52. package/dist/views/Posts/PostTypesView.d.ts +5 -4
  53. package/dist/views/Posts/PostTypesView.d.ts.map +1 -1
  54. package/dist/views/Posts/PostTypesView.js.map +1 -1
  55. package/dist/views/Posts/PostsListView.d.ts.map +1 -1
  56. package/dist/views/Posts/PostsListView.js +8 -0
  57. package/dist/views/Posts/PostsListView.js.map +1 -1
  58. package/dist/views/page-editor/PagePreview.d.ts.map +1 -1
  59. package/dist/views/page-editor/PagePreview.js +10 -1
  60. package/dist/views/page-editor/PagePreview.js.map +1 -1
  61. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  62. package/dist/views/page-editor/PageSectionEditor.js +22 -2
  63. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  64. package/dist/views/page-editor/sections/ArticleBodySection.d.ts +6 -2
  65. package/dist/views/page-editor/sections/ArticleBodySection.d.ts.map +1 -1
  66. package/dist/views/page-editor/sections/ArticleBodySection.js +8 -8
  67. package/dist/views/page-editor/sections/ArticleBodySection.js.map +1 -1
  68. package/dist/views/post-editor/PostEditorCanvas.js +7 -1
  69. package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
  70. package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -1
  71. package/dist/views/post-editor/PostFieldsModal.js +10 -4
  72. package/dist/views/post-editor/PostFieldsModal.js.map +1 -1
  73. package/dist/views/post-editor/PostHeader.d.ts.map +1 -1
  74. package/dist/views/post-editor/PostHeader.js +4 -3
  75. package/dist/views/post-editor/PostHeader.js.map +1 -1
  76. package/dist/views/post-editor/PostPreview.d.ts.map +1 -1
  77. package/dist/views/post-editor/PostPreview.js +11 -1
  78. package/dist/views/post-editor/PostPreview.js.map +1 -1
  79. package/dist/views/post-editor/PostSectionEditor.d.ts +22 -2
  80. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  81. package/dist/views/post-editor/PostSectionEditor.js +4 -1
  82. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  83. package/package.json +2 -2
  84. package/src/__tests__/lib/page-editor-service-api.test.ts +180 -0
  85. package/src/__tests__/lib/pages-service-fetch.test.ts +160 -0
  86. package/src/__tests__/lib/post-editor-service.test.ts +78 -23
  87. package/src/__tests__/lib/posts-service.test.ts +134 -163
  88. package/src/__tests__/views/page-section-editor.test.tsx +65 -0
  89. package/src/__tests__/views/pages-list-view.test.tsx +119 -0
  90. package/src/__tests__/views/posts-list-view.render.test.tsx +84 -0
  91. package/src/index.ts +0 -2
  92. package/src/lib/page-editor-service.ts +61 -0
  93. package/src/lib/pages-service.ts +88 -35
  94. package/src/lib/post-editor-service.ts +41 -40
  95. package/src/lib/posts-service.ts +109 -180
  96. package/src/views/Pages/PagesListView.tsx +113 -82
  97. package/src/views/Posts/NewPostPicker.tsx +5 -4
  98. package/src/views/Posts/PostTypesView.tsx +5 -4
  99. package/src/views/Posts/PostsListView.tsx +9 -0
  100. package/src/views/page-editor/PagePreview.tsx +10 -1
  101. package/src/views/page-editor/PageSectionEditor.tsx +25 -1
  102. package/src/views/page-editor/sections/ArticleBodySection.tsx +12 -16
  103. package/src/views/post-editor/PostEditorCanvas.tsx +7 -1
  104. package/src/views/post-editor/PostFieldsModal.tsx +36 -21
  105. package/src/views/post-editor/PostHeader.tsx +4 -4
  106. package/src/views/post-editor/PostPreview.tsx +12 -1
  107. package/src/views/post-editor/PostSectionEditor.tsx +22 -5
  108. package/dist/views/PostEditor.d.ts +0 -6
  109. package/dist/views/PostEditor.d.ts.map +0 -1
  110. package/dist/views/PostEditor.js +0 -79
  111. package/dist/views/PostEditor.js.map +0 -1
  112. package/dist/views/Posts.d.ts +0 -5
  113. package/dist/views/Posts.d.ts.map +0 -1
  114. package/dist/views/Posts.js +0 -73
  115. package/dist/views/Posts.js.map +0 -1
  116. package/src/views/PostEditor.tsx +0 -257
  117. package/src/views/Posts.tsx +0 -375
@@ -1,375 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- Plus,
5
- Search,
6
- Trash2,
7
- SlidersHorizontal,
8
- Pencil,
9
- ArrowUpDown,
10
- ArrowUp,
11
- ArrowDown,
12
- Loader2,
13
- AlertTriangle,
14
- } from 'lucide-react'
15
- import { useState, useMemo } from 'react'
16
- import { toast } from 'sonner'
17
- import { sortByRelevance, type SortConfig, toggleSort } from '../lib/search.js'
18
- import { useApiData } from '../lib/useApiData.js'
19
- import { cmsApi } from '../lib/api.js'
20
-
21
- type PostSortKey = 'title' | 'author' | 'category' | 'status' | 'date'
22
-
23
- export interface PostsProps {
24
- onNavigate?: (path: string) => void
25
- }
26
-
27
- export function Posts({ onNavigate }: PostsProps) {
28
- const { data, loading, error, refetch } = useApiData<{ docs: any[]; total: number }>(
29
- '/collections/posts?pageSize=100',
30
- )
31
- const [searchQuery, setSearchQuery] = useState('')
32
- const [filterStatus, setFilterStatus] = useState<string>('all')
33
- const [filterCategory, setFilterCategory] = useState<string>('all')
34
- const [selectedPosts, setSelectedPosts] = useState<number[]>([])
35
- const [sortConfig, setSortConfig] = useState<SortConfig<PostSortKey> | null>(null)
36
-
37
- const posts = data?.docs ?? []
38
-
39
- const filteredAndSorted = useMemo(() => {
40
- let results = posts.filter((post: any) => {
41
- const matchesSearch =
42
- (post.title ?? '').toLowerCase().includes(searchQuery.toLowerCase()) ||
43
- (post.author ?? '').toLowerCase().includes(searchQuery.toLowerCase())
44
- const matchesStatus =
45
- filterStatus === 'all' || (post.status ?? '').toLowerCase() === filterStatus.toLowerCase()
46
- const matchesCategory =
47
- filterCategory === 'all' ||
48
- (post.category ?? '').toLowerCase() === filterCategory.toLowerCase()
49
- return matchesSearch && matchesStatus && matchesCategory
50
- })
51
-
52
- if (searchQuery.trim()) {
53
- results = sortByRelevance(results, searchQuery, (p: any) => [
54
- p.title,
55
- p.author ?? '',
56
- p.category ?? '',
57
- ])
58
- } else if (sortConfig) {
59
- results = [...results].sort((a: any, b: any) => {
60
- const aVal = a[sortConfig.key] ?? ''
61
- const bVal = b[sortConfig.key] ?? ''
62
- const cmp = String(aVal).localeCompare(String(bVal))
63
- return sortConfig.direction === 'asc' ? cmp : -cmp
64
- })
65
- }
66
- return results
67
- }, [posts, searchQuery, filterStatus, filterCategory, sortConfig])
68
-
69
- const handleSelectAll = (checked: boolean) => {
70
- setSelectedPosts(checked ? filteredAndSorted.map((p: any) => p.id) : [])
71
- }
72
-
73
- const handleSelectPost = (id: number) => {
74
- setSelectedPosts((prev) =>
75
- prev.includes(id) ? prev.filter((pid) => pid !== id) : [...prev, id],
76
- )
77
- }
78
-
79
- const handleBulkDelete = async () => {
80
- for (const id of selectedPosts) {
81
- await cmsApi(`/collections/posts/${id}`, { method: 'DELETE' })
82
- }
83
- toast.success(`${selectedPosts.length} posts deleted`)
84
- setSelectedPosts([])
85
- refetch()
86
- }
87
-
88
- const handleDelete = async (id: number) => {
89
- await cmsApi(`/collections/posts/${id}`, { method: 'DELETE' })
90
- toast.success('Post deleted')
91
- setSelectedPosts((prev) => prev.filter((pid) => pid !== id))
92
- refetch()
93
- }
94
-
95
- function SortHeader({ label, sortKey }: { label: string; sortKey: PostSortKey }) {
96
- const active = sortConfig?.key === sortKey
97
- return (
98
- <button
99
- type="button"
100
- onClick={() => setSortConfig(toggleSort(sortConfig, sortKey))}
101
- className="flex items-center gap-1 text-xs font-medium text-gray-700 transition-colors hover:text-gray-900"
102
- >
103
- {label}
104
- {active ? (
105
- sortConfig!.direction === 'asc' ? (
106
- <ArrowUp className="h-3 w-3" />
107
- ) : (
108
- <ArrowDown className="h-3 w-3" />
109
- )
110
- ) : (
111
- <ArrowUpDown className="h-3 w-3 text-gray-400" />
112
- )}
113
- </button>
114
- )
115
- }
116
-
117
- if (loading) {
118
- return (
119
- <div className="flex h-64 items-center justify-center p-3 pr-6 sm:p-4 sm:pr-8">
120
- <Loader2 className="h-6 w-6 animate-spin text-blue-600" />
121
- </div>
122
- )
123
- }
124
-
125
- return (
126
- <div className="flex h-full flex-col p-3 pr-6 sm:p-4 sm:pr-8">
127
- {error && (
128
- <div className="mb-4 flex items-center gap-3 rounded-lg border border-red-200 bg-red-50 p-3">
129
- <AlertTriangle className="h-5 w-5 shrink-0 text-red-600" />
130
- <span className="flex-1 text-sm text-red-800">{error}</span>
131
- <button
132
- onClick={refetch}
133
- className="rounded-lg border border-red-300 px-3 py-1 text-sm text-red-700 transition-colors hover:bg-red-100"
134
- >
135
- Retry
136
- </button>
137
- </div>
138
- )}
139
-
140
- <div className="mb-4 flex flex-col justify-between gap-3 sm:flex-row sm:items-center">
141
- <div>
142
- <h1 className="mb-1 text-xl font-semibold text-gray-900 sm:text-2xl">Posts</h1>
143
- <p className="text-sm text-gray-600">{filteredAndSorted.length} total posts</p>
144
- </div>
145
- <button
146
- onClick={() => onNavigate?.('/posts/new')}
147
- className="flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-700"
148
- >
149
- <Plus className="h-4 w-4" />
150
- <span className="hidden sm:inline">New Post</span>
151
- <span className="sm:hidden">New</span>
152
- </button>
153
- </div>
154
-
155
- <div className="mb-4 rounded-lg border border-gray-200 bg-white">
156
- <div className="flex flex-col gap-3 p-3">
157
- <div className="relative">
158
- <Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
159
- <input
160
- type="text"
161
- placeholder="Search posts..."
162
- value={searchQuery}
163
- onChange={(e) => setSearchQuery(e.target.value)}
164
- className="w-full rounded-lg border border-gray-300 py-2 pr-3 pl-9 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
165
- />
166
- </div>
167
- <div className="flex flex-col gap-2 sm:flex-row">
168
- <select
169
- value={filterStatus}
170
- onChange={(e) => setFilterStatus(e.target.value)}
171
- className="flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
172
- >
173
- <option value="all">All Status</option>
174
- <option value="published">Published</option>
175
- <option value="draft">Draft</option>
176
- </select>
177
- <select
178
- value={filterCategory}
179
- onChange={(e) => setFilterCategory(e.target.value)}
180
- className="flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
181
- >
182
- <option value="all">All Categories</option>
183
- <option value="technology">Technology</option>
184
- <option value="design">Design</option>
185
- <option value="business">Business</option>
186
- </select>
187
- <button
188
- type="button"
189
- className="flex items-center justify-center gap-2 rounded-lg border border-gray-300 px-3 py-2 text-sm transition-colors hover:bg-gray-50"
190
- >
191
- <SlidersHorizontal className="h-4 w-4" />
192
- <span className="hidden sm:inline">More Filters</span>
193
- </button>
194
- </div>
195
- </div>
196
- </div>
197
-
198
- {selectedPosts.length > 0 && (
199
- <div className="mb-4 rounded-lg border border-blue-200 bg-blue-50 p-3">
200
- <div className="flex flex-col justify-between gap-2 sm:flex-row sm:items-center">
201
- <span className="text-sm text-blue-900">
202
- {selectedPosts.length} post{selectedPosts.length !== 1 ? 's' : ''} selected
203
- </span>
204
- <div className="flex items-center gap-2">
205
- <button
206
- type="button"
207
- onClick={handleBulkDelete}
208
- className="flex-1 rounded-lg bg-red-600 px-3 py-1.5 text-sm text-white transition-colors hover:bg-red-700 sm:flex-none"
209
- >
210
- Delete
211
- </button>
212
- <button
213
- type="button"
214
- onClick={() => setSelectedPosts([])}
215
- className="flex-1 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm transition-colors hover:bg-gray-50 sm:flex-none"
216
- >
217
- Cancel
218
- </button>
219
- </div>
220
- </div>
221
- </div>
222
- )}
223
-
224
- {filteredAndSorted.length === 0 && !error ? (
225
- <div className="flex flex-1 flex-col items-center justify-center rounded-lg border border-gray-200 bg-white p-8 text-center">
226
- <p className="mb-2 text-sm text-gray-500">No posts yet</p>
227
- <button
228
- onClick={() => onNavigate?.('/posts/new')}
229
- className="rounded-lg bg-blue-600 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-700"
230
- >
231
- Create your first post
232
- </button>
233
- </div>
234
- ) : (
235
- <>
236
- <div className="hidden flex-1 overflow-hidden rounded-lg border border-gray-200 bg-white md:block">
237
- <div className="h-full overflow-x-auto">
238
- <table className="w-full">
239
- <thead className="sticky top-0 border-b border-gray-200 bg-gray-50">
240
- <tr>
241
- <th className="w-8 px-3 py-2 text-left">
242
- <input
243
- type="checkbox"
244
- checked={
245
- selectedPosts.length === filteredAndSorted.length &&
246
- filteredAndSorted.length > 0
247
- }
248
- onChange={(e) => handleSelectAll(e.target.checked)}
249
- className="rounded border-gray-300"
250
- />
251
- </th>
252
- <th className="px-3 py-2 text-left">
253
- <SortHeader label="Title" sortKey="title" />
254
- </th>
255
- <th className="px-3 py-2 text-left">
256
- <SortHeader label="Author" sortKey="author" />
257
- </th>
258
- <th className="px-3 py-2 text-left">
259
- <SortHeader label="Category" sortKey="category" />
260
- </th>
261
- <th className="px-3 py-2 text-left">
262
- <SortHeader label="Status" sortKey="status" />
263
- </th>
264
- <th className="px-3 py-2 text-left">
265
- <SortHeader label="Date" sortKey="date" />
266
- </th>
267
- <th className="px-3 py-2 text-left text-xs font-medium text-gray-700">
268
- Actions
269
- </th>
270
- </tr>
271
- </thead>
272
- <tbody className="divide-y divide-gray-200">
273
- {filteredAndSorted.map((post: any) => (
274
- <tr key={post.id} className="transition-colors hover:bg-gray-50">
275
- <td className="px-3 py-2">
276
- <input
277
- type="checkbox"
278
- checked={selectedPosts.includes(post.id)}
279
- onChange={() => handleSelectPost(post.id)}
280
- className="rounded border-gray-300"
281
- />
282
- </td>
283
- <td className="px-3 py-2">
284
- <button
285
- type="button"
286
- onClick={() => onNavigate?.(`/posts/${post.id}`)}
287
- className="text-left text-sm font-medium text-gray-900 hover:text-blue-600"
288
- >
289
- {post.title}
290
- </button>
291
- </td>
292
- <td className="px-3 py-2 text-sm text-gray-600">{post.author}</td>
293
- <td className="px-3 py-2 text-sm text-gray-600">{post.category}</td>
294
- <td className="px-3 py-2">
295
- <span
296
- className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${post.status === 'Published' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}
297
- >
298
- {post.status}
299
- </span>
300
- </td>
301
- <td className="px-3 py-2 text-sm text-gray-600">{post.date}</td>
302
- <td className="px-3 py-2">
303
- <div className="flex items-center gap-2">
304
- <button
305
- type="button"
306
- onClick={() => onNavigate?.(`/posts/${post.id}`)}
307
- className="rounded p-1.5 transition-colors hover:bg-gray-100"
308
- title="Edit"
309
- >
310
- <Pencil className="h-4 w-4 text-gray-600" />
311
- </button>
312
- <button
313
- type="button"
314
- onClick={() => handleDelete(post.id)}
315
- className="rounded p-1.5 transition-colors hover:bg-gray-100"
316
- title="Delete"
317
- >
318
- <Trash2 className="h-4 w-4 text-red-600" />
319
- </button>
320
- </div>
321
- </td>
322
- </tr>
323
- ))}
324
- </tbody>
325
- </table>
326
- </div>
327
- </div>
328
-
329
- <div className="flex-1 overflow-auto rounded-lg border border-gray-200 bg-white md:hidden">
330
- <div className="divide-y divide-gray-200">
331
- {filteredAndSorted.map((post: any) => (
332
- <div key={post.id} className="p-3">
333
- <div className="flex items-start gap-3">
334
- <input
335
- type="checkbox"
336
- checked={selectedPosts.includes(post.id)}
337
- onChange={() => handleSelectPost(post.id)}
338
- className="mt-1 rounded border-gray-300"
339
- />
340
- <div className="min-w-0 flex-1">
341
- <button
342
- type="button"
343
- onClick={() => onNavigate?.(`/posts/${post.id}`)}
344
- className="mb-1 block text-left text-sm font-medium text-gray-900 hover:text-blue-600"
345
- >
346
- {post.title}
347
- </button>
348
- <div className="mb-2 flex flex-wrap items-center gap-2 text-xs text-gray-600">
349
- <span>{post.author}</span>
350
- <span>•</span>
351
- <span>{post.date}</span>
352
- <span
353
- className={`rounded-full px-2 py-0.5 text-xs font-medium ${post.status === 'Published' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}
354
- >
355
- {post.status}
356
- </span>
357
- </div>
358
- </div>
359
- <button
360
- type="button"
361
- onClick={() => handleDelete(post.id)}
362
- className="rounded p-1.5 transition-colors hover:bg-gray-100"
363
- >
364
- <Trash2 className="h-4 w-4 text-red-600" />
365
- </button>
366
- </div>
367
- </div>
368
- ))}
369
- </div>
370
- </div>
371
- </>
372
- )}
373
- </div>
374
- )
375
- }