@actuate-media/cms-admin 0.2.1 → 0.3.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.
- package/dist/AdminRoot.js +1 -1
- package/dist/AdminRoot.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/ContentOverviewChart.d.ts +8 -0
- package/dist/components/ContentOverviewChart.d.ts.map +1 -0
- package/dist/components/ContentOverviewChart.js +20 -0
- package/dist/components/ContentOverviewChart.js.map +1 -0
- package/dist/components/SEOPerformance.d.ts +5 -0
- package/dist/components/SEOPerformance.d.ts.map +1 -0
- package/dist/components/SEOPerformance.js +24 -0
- package/dist/components/SEOPerformance.js.map +1 -0
- package/dist/views/Dashboard.d.ts +2 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +81 -32
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/Pages.d.ts.map +1 -1
- package/dist/views/Pages.js +57 -2
- package/dist/views/Pages.js.map +1 -1
- package/package.json +3 -2
- package/src/AdminRoot.tsx +1 -1
- package/src/components/ContentOverviewChart.tsx +70 -0
- package/src/components/SEOPerformance.tsx +134 -0
- package/src/views/Dashboard.tsx +175 -192
- package/src/views/Pages.tsx +132 -58
package/src/views/Pages.tsx
CHANGED
|
@@ -14,6 +14,63 @@ export interface PagesProps {
|
|
|
14
14
|
onNavigate?: (path: string) => void;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
const COLOR_PALETTE = [
|
|
18
|
+
{ bg: 'bg-blue-100', text: 'text-blue-700' },
|
|
19
|
+
{ bg: 'bg-green-100', text: 'text-green-700' },
|
|
20
|
+
{ bg: 'bg-purple-100', text: 'text-purple-700' },
|
|
21
|
+
{ bg: 'bg-orange-100', text: 'text-orange-700' },
|
|
22
|
+
{ bg: 'bg-pink-100', text: 'text-pink-700' },
|
|
23
|
+
{ bg: 'bg-teal-100', text: 'text-teal-700' },
|
|
24
|
+
{ bg: 'bg-indigo-100', text: 'text-indigo-700' },
|
|
25
|
+
{ bg: 'bg-rose-100', text: 'text-rose-700' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function hashString(s: string): number {
|
|
29
|
+
let hash = 0;
|
|
30
|
+
for (let i = 0; i < s.length; i++) {
|
|
31
|
+
hash = ((hash << 5) - hash + s.charCodeAt(i)) | 0;
|
|
32
|
+
}
|
|
33
|
+
return Math.abs(hash);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const folderColorCache: Record<string, (typeof COLOR_PALETTE)[0]> = {};
|
|
37
|
+
|
|
38
|
+
function getFolderColor(name: string) {
|
|
39
|
+
if (!folderColorCache[name]) {
|
|
40
|
+
folderColorCache[name] = COLOR_PALETTE[hashString(name) % COLOR_PALETTE.length]!;
|
|
41
|
+
}
|
|
42
|
+
return folderColorCache[name]!;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function computeSeoScore(data: Record<string, unknown> | null | undefined): number {
|
|
46
|
+
if (!data) return 0;
|
|
47
|
+
let score = 0;
|
|
48
|
+
if (data.metaTitle || data.seoTitle) score += 25;
|
|
49
|
+
if (data.metaDescription || data.seoDescription) score += 25;
|
|
50
|
+
if (data.canonical) score += 25;
|
|
51
|
+
if (data.schemaType) score += 25;
|
|
52
|
+
return score;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function SeoScoreBadge({ score }: { score: number }) {
|
|
56
|
+
const color = score >= 80 ? 'bg-green-500' : score >= 60 ? 'bg-amber-500' : 'bg-red-500';
|
|
57
|
+
return (
|
|
58
|
+
<div className="flex items-center gap-1.5">
|
|
59
|
+
<span className={`w-2.5 h-2.5 rounded-full ${color}`} />
|
|
60
|
+
<span className="text-xs text-gray-600">{score}</span>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function FolderBadge({ name }: { name: string }) {
|
|
66
|
+
const colors = getFolderColor(name);
|
|
67
|
+
return (
|
|
68
|
+
<span className={`inline-flex px-2 py-0.5 rounded text-xs font-medium ${colors.bg} ${colors.text}`}>
|
|
69
|
+
{name}
|
|
70
|
+
</span>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
17
74
|
function buildApiUrl(folderSel: FolderSelection): string {
|
|
18
75
|
const base = '/collections/pages?pageSize=100';
|
|
19
76
|
if (folderSel.type === 'smart') {
|
|
@@ -41,6 +98,7 @@ export function Pages({ onNavigate }: PagesProps) {
|
|
|
41
98
|
const [sortConfig, setSortConfig] = useState<SortConfig<PageSortKey> | null>(null);
|
|
42
99
|
|
|
43
100
|
const pages = data?.docs ?? [];
|
|
101
|
+
const totalCount = data?.total ?? pages.length;
|
|
44
102
|
|
|
45
103
|
const filteredAndSorted = useMemo(() => {
|
|
46
104
|
let results = pages.filter((page: any) => {
|
|
@@ -177,8 +235,8 @@ export function Pages({ onNavigate }: PagesProps) {
|
|
|
177
235
|
<FolderInput className="w-5 h-5 text-gray-600" />
|
|
178
236
|
</button>
|
|
179
237
|
<div>
|
|
180
|
-
<h1 className="text-xl sm:text-2xl font-semibold text-gray-900
|
|
181
|
-
<p className="text-sm text-gray-
|
|
238
|
+
<h1 className="text-xl sm:text-2xl font-semibold text-gray-900">Pages</h1>
|
|
239
|
+
<p className="text-sm text-gray-500">{totalCount} page{totalCount !== 1 ? 's' : ''}</p>
|
|
182
240
|
</div>
|
|
183
241
|
</div>
|
|
184
242
|
<button
|
|
@@ -270,48 +328,58 @@ export function Pages({ onNavigate }: PagesProps) {
|
|
|
270
328
|
</th>
|
|
271
329
|
<th className="w-6 px-1 py-2"></th>
|
|
272
330
|
<th className="px-3 py-2 text-left"><SortHeader label="Title" sortKey="title" /></th>
|
|
331
|
+
<th className="px-3 py-2 text-left text-xs font-medium text-gray-700">Folder</th>
|
|
273
332
|
<th className="px-3 py-2 text-left"><SortHeader label="Author" sortKey="author" /></th>
|
|
274
|
-
<th className="px-3 py-2 text-left
|
|
333
|
+
<th className="px-3 py-2 text-left text-xs font-medium text-gray-700">SEO</th>
|
|
275
334
|
<th className="px-3 py-2 text-left"><SortHeader label="Status" sortKey="status" /></th>
|
|
276
335
|
<th className="px-3 py-2 text-left"><SortHeader label="Date" sortKey="date" /></th>
|
|
277
336
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-700">Actions</th>
|
|
278
337
|
</tr>
|
|
279
338
|
</thead>
|
|
280
339
|
<tbody className="divide-y divide-gray-200">
|
|
281
|
-
{filteredAndSorted.map((page: any) =>
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
<
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
<
|
|
305
|
-
<
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
<
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
340
|
+
{filteredAndSorted.map((page: any) => {
|
|
341
|
+
const seoScore = computeSeoScore(page.data);
|
|
342
|
+
const folderName = page.folder?.name ?? page.folderName;
|
|
343
|
+
return (
|
|
344
|
+
<tr
|
|
345
|
+
key={page.id}
|
|
346
|
+
className="hover:bg-gray-50 transition-colors"
|
|
347
|
+
draggable
|
|
348
|
+
onDragStart={(e) => handleDragStart(e, page.id)}
|
|
349
|
+
>
|
|
350
|
+
<td className="px-3 py-2">
|
|
351
|
+
<input type="checkbox" checked={selectedPages.includes(page.id)} onChange={() => handleSelectPage(page.id)} className="rounded border-gray-300" />
|
|
352
|
+
</td>
|
|
353
|
+
<td className="px-1 py-2 cursor-grab">
|
|
354
|
+
<GripVertical className="w-4 h-4 text-gray-300" />
|
|
355
|
+
</td>
|
|
356
|
+
<td className="px-3 py-2">
|
|
357
|
+
<button type="button" onClick={() => onNavigate?.(`/pages/${page.id}`)} className="font-medium text-gray-900 hover:text-blue-600 text-sm text-left">{page.title}</button>
|
|
358
|
+
</td>
|
|
359
|
+
<td className="px-3 py-2">
|
|
360
|
+
{folderName ? <FolderBadge name={folderName} /> : <span className="text-xs text-gray-400">—</span>}
|
|
361
|
+
</td>
|
|
362
|
+
<td className="px-3 py-2 text-sm text-gray-600">{page.author}</td>
|
|
363
|
+
<td className="px-3 py-2">
|
|
364
|
+
<SeoScoreBadge score={seoScore} />
|
|
365
|
+
</td>
|
|
366
|
+
<td className="px-3 py-2">
|
|
367
|
+
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${page.status === 'Published' || page.status === 'PUBLISHED' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}>{page.status}</span>
|
|
368
|
+
</td>
|
|
369
|
+
<td className="px-3 py-2 text-sm text-gray-600">{page.date}</td>
|
|
370
|
+
<td className="px-3 py-2">
|
|
371
|
+
<div className="flex items-center gap-2">
|
|
372
|
+
<button type="button" onClick={() => onNavigate?.(`/pages/${page.id}`)} className="p-1.5 hover:bg-gray-100 rounded transition-colors" title="Edit">
|
|
373
|
+
<Pencil className="w-4 h-4 text-gray-600" />
|
|
374
|
+
</button>
|
|
375
|
+
<button type="button" onClick={() => handleDelete(page.id)} className="p-1.5 hover:bg-gray-100 rounded transition-colors" title="Delete">
|
|
376
|
+
<Trash2 className="w-4 h-4 text-red-600" />
|
|
377
|
+
</button>
|
|
378
|
+
</div>
|
|
379
|
+
</td>
|
|
380
|
+
</tr>
|
|
381
|
+
);
|
|
382
|
+
})}
|
|
315
383
|
</tbody>
|
|
316
384
|
</table>
|
|
317
385
|
</div>
|
|
@@ -319,30 +387,36 @@ export function Pages({ onNavigate }: PagesProps) {
|
|
|
319
387
|
|
|
320
388
|
<div className="md:hidden bg-white rounded-lg border border-gray-200 flex-1 overflow-auto">
|
|
321
389
|
<div className="divide-y divide-gray-200">
|
|
322
|
-
{filteredAndSorted.map((page: any) =>
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
<
|
|
334
|
-
|
|
335
|
-
<
|
|
336
|
-
<
|
|
337
|
-
|
|
390
|
+
{filteredAndSorted.map((page: any) => {
|
|
391
|
+
const seoScore = computeSeoScore(page.data);
|
|
392
|
+
const folderName = page.folder?.name ?? page.folderName;
|
|
393
|
+
return (
|
|
394
|
+
<div
|
|
395
|
+
key={page.id}
|
|
396
|
+
className="p-3"
|
|
397
|
+
draggable
|
|
398
|
+
onDragStart={(e) => handleDragStart(e, page.id)}
|
|
399
|
+
>
|
|
400
|
+
<div className="flex items-start gap-3">
|
|
401
|
+
<input type="checkbox" checked={selectedPages.includes(page.id)} onChange={() => handleSelectPage(page.id)} className="rounded border-gray-300 mt-1" />
|
|
402
|
+
<div className="flex-1 min-w-0">
|
|
403
|
+
<button type="button" onClick={() => onNavigate?.(`/pages/${page.id}`)} className="font-medium text-sm text-gray-900 hover:text-blue-600 block mb-1 text-left">{page.title}</button>
|
|
404
|
+
<div className="flex flex-wrap items-center gap-2 text-xs text-gray-600 mb-2">
|
|
405
|
+
{folderName && <FolderBadge name={folderName} />}
|
|
406
|
+
<span>{page.author}</span>
|
|
407
|
+
<span>·</span>
|
|
408
|
+
<span>{page.date}</span>
|
|
409
|
+
<SeoScoreBadge score={seoScore} />
|
|
410
|
+
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${page.status === 'Published' || page.status === 'PUBLISHED' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}>{page.status}</span>
|
|
411
|
+
</div>
|
|
338
412
|
</div>
|
|
413
|
+
<button type="button" onClick={() => handleDelete(page.id)} className="p-1.5 hover:bg-gray-100 rounded transition-colors">
|
|
414
|
+
<Trash2 className="w-4 h-4 text-red-600" />
|
|
415
|
+
</button>
|
|
339
416
|
</div>
|
|
340
|
-
<button type="button" onClick={() => handleDelete(page.id)} className="p-1.5 hover:bg-gray-100 rounded transition-colors">
|
|
341
|
-
<Trash2 className="w-4 h-4 text-red-600" />
|
|
342
|
-
</button>
|
|
343
417
|
</div>
|
|
344
|
-
|
|
345
|
-
)
|
|
418
|
+
);
|
|
419
|
+
})}
|
|
346
420
|
</div>
|
|
347
421
|
</div>
|
|
348
422
|
</>
|