@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.
@@ -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 mb-1">Pages</h1>
181
- <p className="text-sm text-gray-600">{filteredAndSorted.length} total pages</p>
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"><SortHeader label="Template" sortKey="template" /></th>
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
- <tr
283
- key={page.id}
284
- className="hover:bg-gray-50 transition-colors"
285
- draggable
286
- onDragStart={(e) => handleDragStart(e, page.id)}
287
- >
288
- <td className="px-3 py-2">
289
- <input type="checkbox" checked={selectedPages.includes(page.id)} onChange={() => handleSelectPage(page.id)} className="rounded border-gray-300" />
290
- </td>
291
- <td className="px-1 py-2 cursor-grab">
292
- <GripVertical className="w-4 h-4 text-gray-300" />
293
- </td>
294
- <td className="px-3 py-2">
295
- <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>
296
- </td>
297
- <td className="px-3 py-2 text-sm text-gray-600">{page.author}</td>
298
- <td className="px-3 py-2 text-sm text-gray-600">{page.template}</td>
299
- <td className="px-3 py-2">
300
- <span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${page.status === 'Published' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}>{page.status}</span>
301
- </td>
302
- <td className="px-3 py-2 text-sm text-gray-600">{page.date}</td>
303
- <td className="px-3 py-2">
304
- <div className="flex items-center gap-2">
305
- <button type="button" onClick={() => onNavigate?.(`/pages/${page.id}`)} className="p-1.5 hover:bg-gray-100 rounded transition-colors" title="Edit">
306
- <Pencil className="w-4 h-4 text-gray-600" />
307
- </button>
308
- <button type="button" onClick={() => handleDelete(page.id)} className="p-1.5 hover:bg-gray-100 rounded transition-colors" title="Delete">
309
- <Trash2 className="w-4 h-4 text-red-600" />
310
- </button>
311
- </div>
312
- </td>
313
- </tr>
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
- <div
324
- key={page.id}
325
- className="p-3"
326
- draggable
327
- onDragStart={(e) => handleDragStart(e, page.id)}
328
- >
329
- <div className="flex items-start gap-3">
330
- <input type="checkbox" checked={selectedPages.includes(page.id)} onChange={() => handleSelectPage(page.id)} className="rounded border-gray-300 mt-1" />
331
- <div className="flex-1 min-w-0">
332
- <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>
333
- <div className="flex flex-wrap items-center gap-2 text-xs text-gray-600 mb-2">
334
- <span>{page.author}</span>
335
- <span>•</span>
336
- <span>{page.date}</span>
337
- <span className={`px-2 py-0.5 rounded-full text-xs font-medium ${page.status === 'Published' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}>{page.status}</span>
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>&middot;</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
- </div>
345
- ))}
418
+ );
419
+ })}
346
420
  </div>
347
421
  </div>
348
422
  </>