@actuate-media/cms-admin 0.16.0 → 0.16.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.
@@ -15,7 +15,7 @@
15
15
  * >= 1024 (desktop): stat cards 5-col, main grid is `1fr 320px`
16
16
  */
17
17
 
18
- import { useMemo, useState } from 'react'
18
+ import { useEffect, useMemo, useState } from 'react'
19
19
  import {
20
20
  FileText,
21
21
  File as FileIcon,
@@ -119,6 +119,24 @@ function collectionLabel(col: CollectionMeta, plural = true): string {
119
119
  return col.labels?.singular ?? fallback
120
120
  }
121
121
 
122
+ /**
123
+ * Resolve the correct editor route for a recent/queued document.
124
+ *
125
+ * Post-type documents MUST open in the section-based post editor
126
+ * (`/posts/:type/:id/edit`). Routing them through the generic
127
+ * `/:collection/:id` handler lands them in the legacy `DocumentEdit`
128
+ * view, which has no concept of the section model — opening + saving
129
+ * there silently strips a post's `sections` data. The canonical Pages
130
+ * collection opens in the full page editor (which wires up preview).
131
+ * Everything else falls through to the existing generic collection
132
+ * route, which already resolves correctly.
133
+ */
134
+ function editPathForDoc(col: CollectionMeta | undefined, collection: string, id: string): string {
135
+ if (col?.type === 'post') return `/posts/${collection}/${id}/edit`
136
+ if (collection === 'pages') return `/pages/${id}/edit`
137
+ return `/${collection}/${id}`
138
+ }
139
+
122
140
  function relativeTime(dateStr: string): string {
123
141
  const then = new Date(dateStr).getTime()
124
142
  const diff = Date.now() - then
@@ -252,8 +270,18 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
252
270
  const { data: contentHealthData, loading: contentHealthLoading } =
253
271
  useApiData<ContentHealthData>('/stats/content-health')
254
272
 
255
- const greeting = useMemo(() => timeOfDayGreeting(), [])
256
- const dateStr = useMemo(() => todayDateString(), [])
273
+ // A slow ticker (60s) so the greeting, calendar date, and "x minutes
274
+ // ago" relative times stay correct on a dashboard left open for hours
275
+ // — without it, a tab opened at 11:58am would read "Good morning" all
276
+ // afternoon and show stale activity timestamps.
277
+ const [now, setNow] = useState(() => Date.now())
278
+ useEffect(() => {
279
+ const id = setInterval(() => setNow(Date.now()), 60_000)
280
+ return () => clearInterval(id)
281
+ }, [])
282
+
283
+ const greeting = useMemo(() => timeOfDayGreeting(), [now])
284
+ const dateStr = useMemo(() => todayDateString(), [now])
257
285
  const userName = useMemo(() => firstNameForGreeting(session), [session])
258
286
 
259
287
  const collections = useMemo(() => resolveCollections(config), [config])
@@ -396,9 +424,12 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
396
424
  relTime: relativeTime(d.updatedAt),
397
425
  avatar: authorAvatar(d.author),
398
426
  statusInfo: statusBadge(d.status),
427
+ editPath: editPathForDoc(col, d.collection, d.id),
399
428
  }
400
429
  })
401
- }, [stats, collections, activityLimit])
430
+ // `now` keeps the relative timestamps fresh on a long-open tab.
431
+ // eslint-disable-next-line react-hooks/exhaustive-deps
432
+ }, [stats, collections, activityLimit, now])
402
433
 
403
434
  // ── Publishing queue (scheduled docs only) ──────────────────────────────
404
435
  const publishQueue = useMemo(() => {
@@ -415,9 +446,11 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
415
446
  type: col ? collectionLabel(col, false) : d.collection,
416
447
  date: relativeTime(d.updatedAt),
417
448
  author: d.author,
449
+ editPath: editPathForDoc(col, d.collection, d.id),
418
450
  }
419
451
  })
420
- }, [stats, collections])
452
+ // eslint-disable-next-line react-hooks/exhaustive-deps
453
+ }, [stats, collections, now])
421
454
 
422
455
  // ── Content health (sourced from /stats/content-health) ─────────────────
423
456
  // Persisted by the nightly content-health cron. The four issue rows
@@ -618,12 +651,14 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
618
651
  <h2 className="text-foreground text-sm font-semibold">Recent Activity</h2>
619
652
  <p className="text-muted-foreground mt-0.5 text-xs">Last 7 days across all content</p>
620
653
  </div>
621
- <button
622
- className="text-xs font-medium text-violet-600 hover:underline dark:text-violet-400"
623
- onClick={() => setActivityLimit((n) => (n >= 20 ? 8 : 20))}
624
- >
625
- {activity.length >= 20 ? 'Show less' : 'View all'}
626
- </button>
654
+ {(stats?.recentDocuments?.length ?? 0) > 8 && (
655
+ <button
656
+ className="text-xs font-medium text-violet-600 hover:underline dark:text-violet-400"
657
+ onClick={() => setActivityLimit((n) => (n >= 20 ? 8 : 20))}
658
+ >
659
+ {activityLimit >= 20 ? 'Show less' : 'Show more'}
660
+ </button>
661
+ )}
627
662
  </header>
628
663
  {activity.length === 0 ? (
629
664
  <EmptyState
@@ -634,38 +669,40 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
634
669
  ) : (
635
670
  <ul role="list" className="divide-border divide-y">
636
671
  {activity.map((it) => (
637
- <li
638
- key={it.id}
639
- className="hover:bg-accent/50 cursor-pointer px-4 py-3 transition-colors"
640
- onClick={() => nav(`/${it.collection}/${it.id}`)}
641
- >
642
- <div className="flex min-w-0 items-start gap-3">
643
- <div
644
- className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-bold text-white"
645
- style={{ background: it.avatar.color }}
646
- aria-hidden
647
- >
648
- {it.avatar.initials}
649
- </div>
650
- <div className="min-w-0 flex-1">
651
- <p className="text-foreground truncate text-sm leading-snug">
652
- <span className="font-semibold">
653
- &ldquo;{it.title || 'Untitled'}&rdquo;
654
- </span>{' '}
655
- <span className="text-muted-foreground">— {it.typeLabel}</span>
656
- </p>
657
- <div className="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-[11px]">
658
- <span
659
- className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${it.statusInfo.cls}`}
660
- >
661
- {it.statusInfo.label}
662
- </span>
663
- <span className="max-w-[120px] truncate">{it.author}</span>
664
- <span aria-hidden>·</span>
665
- <span>{it.relTime}</span>
672
+ <li key={it.id}>
673
+ <button
674
+ type="button"
675
+ onClick={() => nav(it.editPath)}
676
+ className="hover:bg-accent/50 focus-visible:bg-accent/50 w-full cursor-pointer px-4 py-3 text-left transition-colors"
677
+ >
678
+ <div className="flex min-w-0 items-start gap-3">
679
+ <div
680
+ className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-bold text-white"
681
+ style={{ background: it.avatar.color }}
682
+ aria-hidden
683
+ >
684
+ {it.avatar.initials}
685
+ </div>
686
+ <div className="min-w-0 flex-1">
687
+ <p className="text-foreground truncate text-sm leading-snug">
688
+ <span className="font-semibold">
689
+ &ldquo;{it.title || 'Untitled'}&rdquo;
690
+ </span>{' '}
691
+ <span className="text-muted-foreground">— {it.typeLabel}</span>
692
+ </p>
693
+ <div className="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-[11px]">
694
+ <span
695
+ className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${it.statusInfo.cls}`}
696
+ >
697
+ {it.statusInfo.label}
698
+ </span>
699
+ <span className="max-w-[120px] truncate">{it.author}</span>
700
+ <span aria-hidden>·</span>
701
+ <span>{it.relTime}</span>
702
+ </div>
666
703
  </div>
667
704
  </div>
668
- </div>
705
+ </button>
669
706
  </li>
670
707
  ))}
671
708
  </ul>
@@ -691,7 +728,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
691
728
  // link to the first scheduled item — that's where authors
692
729
  // typically need to land to reschedule or cancel.
693
730
  const first = publishQueue[0]
694
- if (first) nav(`/${first.collection}/${first.id}`)
731
+ if (first) nav(first.editPath)
695
732
  }}
696
733
  >
697
734
  Manage
@@ -707,21 +744,23 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
707
744
  ) : (
708
745
  <ul role="list" className="divide-border divide-y">
709
746
  {publishQueue.map((q) => (
710
- <li
711
- key={q.id}
712
- className="hover:bg-accent/50 flex min-w-0 cursor-pointer items-center gap-2.5 px-4 py-2.5 transition-colors"
713
- onClick={() => nav(`/${q.collection}/${q.id}`)}
714
- >
715
- <span className="h-2 w-2 shrink-0 rounded-full bg-amber-500" aria-hidden />
716
- <div className="min-w-0 flex-1">
717
- <p className="text-foreground truncate text-sm font-medium">{q.title}</p>
718
- <p className="text-muted-foreground truncate text-[11px]">
719
- {q.date} · {q.author}
720
- </p>
721
- </div>
722
- <span className="border-border bg-background text-muted-foreground shrink-0 rounded border px-1.5 py-0.5 text-[10px] capitalize">
723
- {q.type}
724
- </span>
747
+ <li key={q.id}>
748
+ <button
749
+ type="button"
750
+ onClick={() => nav(q.editPath)}
751
+ className="hover:bg-accent/50 focus-visible:bg-accent/50 flex w-full min-w-0 cursor-pointer items-center gap-2.5 px-4 py-2.5 text-left transition-colors"
752
+ >
753
+ <span className="h-2 w-2 shrink-0 rounded-full bg-amber-500" aria-hidden />
754
+ <div className="min-w-0 flex-1">
755
+ <p className="text-foreground truncate text-sm font-medium">{q.title}</p>
756
+ <p className="text-muted-foreground truncate text-[11px]">
757
+ {q.date} · {q.author}
758
+ </p>
759
+ </div>
760
+ <span className="border-border bg-background text-muted-foreground shrink-0 rounded border px-1.5 py-0.5 text-[10px] capitalize">
761
+ {q.type}
762
+ </span>
763
+ </button>
725
764
  </li>
726
765
  ))}
727
766
  </ul>
@@ -786,36 +825,38 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
786
825
  ) : (
787
826
  <ul role="list" className="divide-border divide-y">
788
827
  {contentHealth.issues.map((iss, i) => (
789
- <li
790
- key={i}
791
- className="hover:bg-accent/50 flex cursor-pointer items-center gap-2.5 px-4 py-2 transition-colors"
792
- onClick={() => nav('/seo')}
793
- >
794
- <span
795
- className={`h-1.5 w-1.5 shrink-0 rounded-full ${
796
- iss.tone === 'err'
797
- ? 'bg-red-500'
798
- : iss.tone === 'warn'
799
- ? 'bg-amber-500'
800
- : 'bg-muted-foreground'
801
- }`}
802
- aria-hidden
803
- />
804
- <span className="text-muted-foreground flex-1 truncate text-xs">
805
- {iss.label}
806
- </span>
807
- <span
808
- className={`shrink-0 text-sm font-semibold ${
809
- iss.tone === 'err'
810
- ? 'text-red-600 dark:text-red-400'
811
- : iss.tone === 'warn'
812
- ? 'text-amber-600 dark:text-amber-400'
813
- : 'text-muted-foreground'
814
- }`}
828
+ <li key={i}>
829
+ <button
830
+ type="button"
831
+ onClick={() => nav('/seo')}
832
+ className="hover:bg-accent/50 focus-visible:bg-accent/50 flex w-full items-center gap-2.5 px-4 py-2 text-left transition-colors"
815
833
  >
816
- {iss.count}
817
- </span>
818
- <ChevronRight className="text-muted-foreground/60 h-3.5 w-3.5 shrink-0" />
834
+ <span
835
+ className={`h-1.5 w-1.5 shrink-0 rounded-full ${
836
+ iss.tone === 'err'
837
+ ? 'bg-red-500'
838
+ : iss.tone === 'warn'
839
+ ? 'bg-amber-500'
840
+ : 'bg-muted-foreground'
841
+ }`}
842
+ aria-hidden
843
+ />
844
+ <span className="text-muted-foreground flex-1 truncate text-xs">
845
+ {iss.label}
846
+ </span>
847
+ <span
848
+ className={`shrink-0 text-sm font-semibold ${
849
+ iss.tone === 'err'
850
+ ? 'text-red-600 dark:text-red-400'
851
+ : iss.tone === 'warn'
852
+ ? 'text-amber-600 dark:text-amber-400'
853
+ : 'text-muted-foreground'
854
+ }`}
855
+ >
856
+ {iss.count}
857
+ </span>
858
+ <ChevronRight className="text-muted-foreground/60 h-3.5 w-3.5 shrink-0" />
859
+ </button>
819
860
  </li>
820
861
  ))}
821
862
  </ul>