@actuate-media/cms-admin 0.66.0 → 0.67.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/CHANGELOG.md +21 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +7 -4
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/components/inspector-pane.test.d.ts +2 -0
- package/dist/__tests__/components/inspector-pane.test.d.ts.map +1 -0
- package/dist/__tests__/components/inspector-pane.test.js +31 -0
- package/dist/__tests__/components/inspector-pane.test.js.map +1 -0
- package/dist/__tests__/lib/pages-tree.test.d.ts +2 -0
- package/dist/__tests__/lib/pages-tree.test.d.ts.map +1 -0
- package/dist/__tests__/lib/pages-tree.test.js +173 -0
- package/dist/__tests__/lib/pages-tree.test.js.map +1 -0
- package/dist/__tests__/views/dashboard.test.js +7 -3
- package/dist/__tests__/views/dashboard.test.js.map +1 -1
- package/dist/__tests__/views/media-seo-audit.render.test.d.ts +2 -0
- package/dist/__tests__/views/media-seo-audit.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/media-seo-audit.render.test.js +102 -0
- package/dist/__tests__/views/media-seo-audit.render.test.js.map +1 -0
- package/dist/__tests__/views/onboarding-block.render.test.d.ts +2 -0
- package/dist/__tests__/views/onboarding-block.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/onboarding-block.render.test.js +146 -0
- package/dist/__tests__/views/onboarding-block.render.test.js.map +1 -0
- package/dist/__tests__/views/pages-list-view.test.js +77 -1
- package/dist/__tests__/views/pages-list-view.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/script-tags/ScriptTagEditorPane.d.ts +26 -0
- package/dist/components/script-tags/ScriptTagEditorPane.d.ts.map +1 -0
- package/dist/components/script-tags/ScriptTagEditorPane.js +160 -0
- package/dist/components/script-tags/ScriptTagEditorPane.js.map +1 -0
- package/dist/components/ui/InspectorPane.d.ts +51 -0
- package/dist/components/ui/InspectorPane.d.ts.map +1 -0
- package/dist/components/ui/InspectorPane.js +42 -0
- package/dist/components/ui/InspectorPane.js.map +1 -0
- package/dist/components/ui/index.d.ts +2 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +1 -0
- package/dist/components/ui/index.js.map +1 -1
- package/dist/lib/page-editor-service.d.ts +11 -2
- package/dist/lib/page-editor-service.d.ts.map +1 -1
- package/dist/lib/page-editor-service.js +8 -2
- package/dist/lib/page-editor-service.js.map +1 -1
- package/dist/lib/pages-tree.d.ts +44 -0
- package/dist/lib/pages-tree.d.ts.map +1 -0
- package/dist/lib/pages-tree.js +106 -0
- package/dist/lib/pages-tree.js.map +1 -0
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +31 -2
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +28 -2
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/MediaSeoAudit.d.ts +25 -0
- package/dist/views/MediaSeoAudit.d.ts.map +1 -0
- package/dist/views/MediaSeoAudit.js +97 -0
- package/dist/views/MediaSeoAudit.js.map +1 -0
- package/dist/views/Pages/PagesListView.d.ts.map +1 -1
- package/dist/views/Pages/PagesListView.js +90 -10
- package/dist/views/Pages/PagesListView.js.map +1 -1
- package/dist/views/Pages/PagesTreeTable.d.ts +35 -0
- package/dist/views/Pages/PagesTreeTable.d.ts.map +1 -0
- package/dist/views/Pages/PagesTreeTable.js +28 -0
- package/dist/views/Pages/PagesTreeTable.js.map +1 -0
- package/dist/views/ScriptTags.d.ts +5 -1
- package/dist/views/ScriptTags.d.ts.map +1 -1
- package/dist/views/ScriptTags.js +32 -7
- package/dist/views/ScriptTags.js.map +1 -1
- package/dist/views/dashboard/OnboardingBlock.d.ts +29 -0
- package/dist/views/dashboard/OnboardingBlock.d.ts.map +1 -0
- package/dist/views/dashboard/OnboardingBlock.js +71 -0
- package/dist/views/dashboard/OnboardingBlock.js.map +1 -0
- package/dist/views/page-editor/PageSectionEditor.d.ts +7 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +3 -3
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/PageSettingsModal.d.ts +1 -1
- package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -1
- package/dist/views/page-editor/PageSettingsModal.js +42 -3
- package/dist/views/page-editor/PageSettingsModal.js.map +1 -1
- package/package.json +2 -2
- package/src/AdminRoot.tsx +7 -3
- package/src/__tests__/components/inspector-pane.test.tsx +64 -0
- package/src/__tests__/lib/pages-tree.test.ts +197 -0
- package/src/__tests__/views/dashboard.test.tsx +7 -3
- package/src/__tests__/views/media-seo-audit.render.test.tsx +117 -0
- package/src/__tests__/views/onboarding-block.render.test.tsx +193 -0
- package/src/__tests__/views/pages-list-view.test.tsx +93 -1
- package/src/components/script-tags/ScriptTagEditorPane.tsx +474 -0
- package/src/components/ui/InspectorPane.tsx +129 -0
- package/src/components/ui/index.ts +10 -0
- package/src/lib/page-editor-service.ts +16 -2
- package/src/lib/pages-tree.ts +145 -0
- package/src/views/Dashboard.tsx +238 -196
- package/src/views/MediaBrowser.tsx +641 -511
- package/src/views/MediaSeoAudit.tsx +320 -0
- package/src/views/Pages/PagesListView.tsx +352 -183
- package/src/views/Pages/PagesTreeTable.tsx +339 -0
- package/src/views/ScriptTags.tsx +204 -129
- package/src/views/dashboard/OnboardingBlock.tsx +233 -0
- package/src/views/page-editor/PageSectionEditor.tsx +9 -2
- package/src/views/page-editor/PageSettingsModal.tsx +75 -2
- package/dist/views/ScriptTagEditor.d.ts +0 -6
- package/dist/views/ScriptTagEditor.d.ts.map +0 -1
- package/dist/views/ScriptTagEditor.js +0 -111
- package/dist/views/ScriptTagEditor.js.map +0 -1
- package/src/views/ScriptTagEditor.tsx +0 -353
package/src/views/Dashboard.tsx
CHANGED
|
@@ -41,6 +41,8 @@ import {
|
|
|
41
41
|
} from 'lucide-react'
|
|
42
42
|
import { useApiData } from '../lib/useApiData.js'
|
|
43
43
|
import { editPathForDoc } from '../lib/collection-routes.js'
|
|
44
|
+
import { OnboardingBlock, shouldShowOnboarding } from './dashboard/OnboardingBlock.js'
|
|
45
|
+
import type { OnboardingSignals } from './dashboard/OnboardingBlock.js'
|
|
44
46
|
|
|
45
47
|
interface DashboardStats {
|
|
46
48
|
totalDocuments: number
|
|
@@ -521,6 +523,40 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
521
523
|
|
|
522
524
|
const totalIssues = contentHealth.issues.reduce((s, i) => s + i.count, 0)
|
|
523
525
|
|
|
526
|
+
// ── Onboarding (empty-site getting-started block) ───────────────────────
|
|
527
|
+
// Signals come exclusively from data already fetched above (/stats +
|
|
528
|
+
// /stats/api-delivery) — see OnboardingBlock.tsx for the per-step
|
|
529
|
+
// derivation rationale. When the site is effectively empty (zero
|
|
530
|
+
// posts+pages AND zero media) the onboarding block REPLACES the
|
|
531
|
+
// Recent Activity / Publishing Queue / Content Health grid.
|
|
532
|
+
const onboardingSignals: OnboardingSignals = useMemo(() => {
|
|
533
|
+
const counts = stats?.collectionCounts ?? {}
|
|
534
|
+
const contentCollections = collections.filter(
|
|
535
|
+
(c) => c.type === 'post' || c.type === 'page' || c.slug === 'posts' || c.slug === 'pages',
|
|
536
|
+
)
|
|
537
|
+
// No post/page collections configured → fall back to the total document
|
|
538
|
+
// count so a populated site never regresses to the onboarding view.
|
|
539
|
+
const postsAndPagesCount =
|
|
540
|
+
contentCollections.length > 0
|
|
541
|
+
? contentCollections.reduce((sum, c) => sum + (counts[c.slug] ?? 0), 0)
|
|
542
|
+
: (stats?.totalDocuments ?? 0)
|
|
543
|
+
return {
|
|
544
|
+
postsAndPagesCount,
|
|
545
|
+
mediaCount: stats?.totalMedia ?? 0,
|
|
546
|
+
seoScore: stats?.avgSeoScore ?? 0,
|
|
547
|
+
userCount: stats?.totalUsers ?? 0,
|
|
548
|
+
hasApiTraffic: delivery?.hasData ?? false,
|
|
549
|
+
}
|
|
550
|
+
}, [stats, collections, delivery])
|
|
551
|
+
|
|
552
|
+
// Belt-and-braces: any recent activity means the site is not empty even
|
|
553
|
+
// if collection counts are missing/stale, so never hide it behind
|
|
554
|
+
// onboarding.
|
|
555
|
+
const showOnboarding =
|
|
556
|
+
stats !== null &&
|
|
557
|
+
(stats.recentDocuments?.length ?? 0) === 0 &&
|
|
558
|
+
shouldShowOnboarding(onboardingSignals)
|
|
559
|
+
|
|
524
560
|
// ── Render ──────────────────────────────────────────────────────────────
|
|
525
561
|
|
|
526
562
|
if (loading && !stats) {
|
|
@@ -671,124 +707,71 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
671
707
|
})}
|
|
672
708
|
</div>
|
|
673
709
|
|
|
674
|
-
{/*
|
|
675
|
-
|
|
676
|
-
{
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
<h2 className="text-foreground text-sm font-medium">Recent Activity</h2>
|
|
681
|
-
<p className="text-muted-foreground mt-0.5 text-xs">Last 7 days across all content</p>
|
|
682
|
-
</div>
|
|
683
|
-
{(stats?.recentDocuments?.length ?? 0) > 8 && (
|
|
684
|
-
<button
|
|
685
|
-
className="text-brand text-xs font-medium hover:underline"
|
|
686
|
-
onClick={() => setActivityLimit((n) => (n >= 20 ? 8 : 20))}
|
|
687
|
-
>
|
|
688
|
-
{activityLimit >= 20 ? 'Show less' : 'Show more'}
|
|
689
|
-
</button>
|
|
690
|
-
)}
|
|
691
|
-
</header>
|
|
692
|
-
{activity.length === 0 ? (
|
|
693
|
-
<EmptyState
|
|
694
|
-
icon={Activity}
|
|
695
|
-
title="No activity yet"
|
|
696
|
-
subtitle="Content changes will appear here as your team works."
|
|
697
|
-
/>
|
|
698
|
-
) : (
|
|
699
|
-
<ul role="list" className="divide-border divide-y">
|
|
700
|
-
{activity.map((it) => (
|
|
701
|
-
<li key={it.id}>
|
|
702
|
-
<button
|
|
703
|
-
type="button"
|
|
704
|
-
onClick={() => nav(it.editPath)}
|
|
705
|
-
className="hover:bg-accent/50 focus-visible:bg-accent/50 w-full cursor-pointer px-4 py-3 text-left transition-colors"
|
|
706
|
-
>
|
|
707
|
-
<div className="flex min-w-0 items-start gap-3">
|
|
708
|
-
<div
|
|
709
|
-
className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-medium text-white"
|
|
710
|
-
style={{ background: it.avatar.color }}
|
|
711
|
-
aria-hidden
|
|
712
|
-
>
|
|
713
|
-
{it.avatar.initials}
|
|
714
|
-
</div>
|
|
715
|
-
<div className="min-w-0 flex-1">
|
|
716
|
-
<p className="text-foreground truncate text-sm leading-snug">
|
|
717
|
-
<span className="font-medium">
|
|
718
|
-
“{it.title || 'Untitled'}”
|
|
719
|
-
</span>{' '}
|
|
720
|
-
<span className="text-muted-foreground">— {it.typeLabel}</span>
|
|
721
|
-
</p>
|
|
722
|
-
<div className="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-[11px]">
|
|
723
|
-
<span
|
|
724
|
-
className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${it.statusInfo.cls}`}
|
|
725
|
-
>
|
|
726
|
-
{it.statusInfo.label}
|
|
727
|
-
</span>
|
|
728
|
-
<span className="max-w-[120px] truncate">{it.author}</span>
|
|
729
|
-
<span aria-hidden>·</span>
|
|
730
|
-
<span>{it.relTime}</span>
|
|
731
|
-
</div>
|
|
732
|
-
</div>
|
|
733
|
-
</div>
|
|
734
|
-
</button>
|
|
735
|
-
</li>
|
|
736
|
-
))}
|
|
737
|
-
</ul>
|
|
738
|
-
)}
|
|
739
|
-
</section>
|
|
740
|
-
|
|
741
|
-
{/* Right column ─────────────────────────────────────────── */}
|
|
742
|
-
<aside className="flex min-w-0 flex-col gap-4">
|
|
743
|
-
{/* Publishing Queue */}
|
|
710
|
+
{/* Onboarding (empty site) or the main grid ──────────────────── */}
|
|
711
|
+
{showOnboarding ? (
|
|
712
|
+
<OnboardingBlock signals={onboardingSignals} onNavigate={onNavigate} />
|
|
713
|
+
) : (
|
|
714
|
+
<div className="grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_320px]">
|
|
715
|
+
{/* Recent Activity ───────────────────────────────────────── */}
|
|
744
716
|
<section className="bg-card border-border overflow-hidden rounded-xl border shadow-sm">
|
|
745
717
|
<header className="border-border flex items-center justify-between border-b px-4 py-3">
|
|
746
718
|
<div className="min-w-0">
|
|
747
|
-
<h2 className="text-foreground text-sm font-medium">
|
|
719
|
+
<h2 className="text-foreground text-sm font-medium">Recent Activity</h2>
|
|
748
720
|
<p className="text-muted-foreground mt-0.5 text-xs">
|
|
749
|
-
|
|
721
|
+
Last 7 days across all content
|
|
750
722
|
</p>
|
|
751
723
|
</div>
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
if (first) nav(first.editPath)
|
|
761
|
-
}}
|
|
762
|
-
>
|
|
763
|
-
Manage
|
|
764
|
-
</button>
|
|
724
|
+
{(stats?.recentDocuments?.length ?? 0) > 8 && (
|
|
725
|
+
<button
|
|
726
|
+
className="text-brand text-xs font-medium hover:underline"
|
|
727
|
+
onClick={() => setActivityLimit((n) => (n >= 20 ? 8 : 20))}
|
|
728
|
+
>
|
|
729
|
+
{activityLimit >= 20 ? 'Show less' : 'Show more'}
|
|
730
|
+
</button>
|
|
731
|
+
)}
|
|
765
732
|
</header>
|
|
766
|
-
{
|
|
733
|
+
{activity.length === 0 ? (
|
|
767
734
|
<EmptyState
|
|
768
|
-
icon={
|
|
769
|
-
title="No
|
|
770
|
-
subtitle="
|
|
771
|
-
compact
|
|
735
|
+
icon={Activity}
|
|
736
|
+
title="No activity yet"
|
|
737
|
+
subtitle="Content changes will appear here as your team works."
|
|
772
738
|
/>
|
|
773
739
|
) : (
|
|
774
740
|
<ul role="list" className="divide-border divide-y">
|
|
775
|
-
{
|
|
776
|
-
<li key={
|
|
741
|
+
{activity.map((it) => (
|
|
742
|
+
<li key={it.id}>
|
|
777
743
|
<button
|
|
778
744
|
type="button"
|
|
779
|
-
onClick={() => nav(
|
|
780
|
-
className="hover:bg-accent/50 focus-visible:bg-accent/50
|
|
745
|
+
onClick={() => nav(it.editPath)}
|
|
746
|
+
className="hover:bg-accent/50 focus-visible:bg-accent/50 w-full cursor-pointer px-4 py-3 text-left transition-colors"
|
|
781
747
|
>
|
|
782
|
-
<
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
748
|
+
<div className="flex min-w-0 items-start gap-3">
|
|
749
|
+
<div
|
|
750
|
+
className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-medium text-white"
|
|
751
|
+
style={{ background: it.avatar.color }}
|
|
752
|
+
aria-hidden
|
|
753
|
+
>
|
|
754
|
+
{it.avatar.initials}
|
|
755
|
+
</div>
|
|
756
|
+
<div className="min-w-0 flex-1">
|
|
757
|
+
<p className="text-foreground truncate text-sm leading-snug">
|
|
758
|
+
<span className="font-medium">
|
|
759
|
+
“{it.title || 'Untitled'}”
|
|
760
|
+
</span>{' '}
|
|
761
|
+
<span className="text-muted-foreground">— {it.typeLabel}</span>
|
|
762
|
+
</p>
|
|
763
|
+
<div className="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-[11px]">
|
|
764
|
+
<span
|
|
765
|
+
className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${it.statusInfo.cls}`}
|
|
766
|
+
>
|
|
767
|
+
{it.statusInfo.label}
|
|
768
|
+
</span>
|
|
769
|
+
<span className="max-w-[120px] truncate">{it.author}</span>
|
|
770
|
+
<span aria-hidden>·</span>
|
|
771
|
+
<span>{it.relTime}</span>
|
|
772
|
+
</div>
|
|
773
|
+
</div>
|
|
788
774
|
</div>
|
|
789
|
-
<span className="border-border bg-background text-muted-foreground shrink-0 rounded border px-1.5 py-0.5 text-[10px] capitalize">
|
|
790
|
-
{q.type}
|
|
791
|
-
</span>
|
|
792
775
|
</button>
|
|
793
776
|
</li>
|
|
794
777
|
))}
|
|
@@ -796,103 +779,162 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
796
779
|
)}
|
|
797
780
|
</section>
|
|
798
781
|
|
|
799
|
-
{/*
|
|
800
|
-
<
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
<
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
</p>
|
|
821
|
-
<span
|
|
822
|
-
className={`text-xs font-medium ${
|
|
823
|
-
contentHealth.tone === 'ok'
|
|
824
|
-
? 'text-success'
|
|
825
|
-
: contentHealth.tone === 'warn'
|
|
826
|
-
? 'text-warning'
|
|
827
|
-
: contentHealth.tone === 'err'
|
|
828
|
-
? 'text-destructive dark:text-red-400'
|
|
829
|
-
: 'text-muted-foreground'
|
|
830
|
-
}`}
|
|
782
|
+
{/* Right column ─────────────────────────────────────────── */}
|
|
783
|
+
<aside className="flex min-w-0 flex-col gap-4">
|
|
784
|
+
{/* Publishing Queue */}
|
|
785
|
+
<section className="bg-card border-border overflow-hidden rounded-xl border shadow-sm">
|
|
786
|
+
<header className="border-border flex items-center justify-between border-b px-4 py-3">
|
|
787
|
+
<div className="min-w-0">
|
|
788
|
+
<h2 className="text-foreground text-sm font-medium">Publishing Queue</h2>
|
|
789
|
+
<p className="text-muted-foreground mt-0.5 text-xs">
|
|
790
|
+
{publishQueue.length} item{publishQueue.length === 1 ? '' : 's'} scheduled
|
|
791
|
+
</p>
|
|
792
|
+
</div>
|
|
793
|
+
<button
|
|
794
|
+
className="text-brand text-xs font-medium hover:underline disabled:cursor-default disabled:no-underline disabled:opacity-50"
|
|
795
|
+
disabled={publishQueue.length === 0}
|
|
796
|
+
onClick={() => {
|
|
797
|
+
// We don't have a dedicated "scheduled" admin page, so deep-
|
|
798
|
+
// link to the first scheduled item — that's where authors
|
|
799
|
+
// typically need to land to reschedule or cancel.
|
|
800
|
+
const first = publishQueue[0]
|
|
801
|
+
if (first) nav(first.editPath)
|
|
802
|
+
}}
|
|
831
803
|
>
|
|
832
|
-
|
|
833
|
-
</
|
|
834
|
-
</
|
|
835
|
-
|
|
836
|
-
<
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
? 'bg-warning'
|
|
842
|
-
: contentHealth.tone === 'err'
|
|
843
|
-
? 'bg-destructive'
|
|
844
|
-
: 'bg-muted-foreground/40'
|
|
845
|
-
}`}
|
|
846
|
-
style={{ width: `${Math.max(0, Math.min(100, contentHealth.score))}%` }}
|
|
804
|
+
Manage
|
|
805
|
+
</button>
|
|
806
|
+
</header>
|
|
807
|
+
{publishQueue.length === 0 ? (
|
|
808
|
+
<EmptyState
|
|
809
|
+
icon={Clock}
|
|
810
|
+
title="No scheduled content"
|
|
811
|
+
subtitle="Scheduled posts and pages appear here."
|
|
812
|
+
compact
|
|
847
813
|
/>
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
className=
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
814
|
+
) : (
|
|
815
|
+
<ul role="list" className="divide-border divide-y">
|
|
816
|
+
{publishQueue.map((q) => (
|
|
817
|
+
<li key={q.id}>
|
|
818
|
+
<button
|
|
819
|
+
type="button"
|
|
820
|
+
onClick={() => nav(q.editPath)}
|
|
821
|
+
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"
|
|
822
|
+
>
|
|
823
|
+
<span className="bg-warning h-2 w-2 shrink-0 rounded-full" aria-hidden />
|
|
824
|
+
<div className="min-w-0 flex-1">
|
|
825
|
+
<p className="text-foreground truncate text-sm font-medium">{q.title}</p>
|
|
826
|
+
<p className="text-muted-foreground truncate text-[11px]">
|
|
827
|
+
{q.date} · {q.author}
|
|
828
|
+
</p>
|
|
829
|
+
</div>
|
|
830
|
+
<span className="border-border bg-background text-muted-foreground shrink-0 rounded border px-1.5 py-0.5 text-[10px] capitalize">
|
|
831
|
+
{q.type}
|
|
832
|
+
</span>
|
|
833
|
+
</button>
|
|
834
|
+
</li>
|
|
835
|
+
))}
|
|
836
|
+
</ul>
|
|
837
|
+
)}
|
|
838
|
+
</section>
|
|
839
|
+
|
|
840
|
+
{/* Content Health */}
|
|
841
|
+
<section className="bg-card border-border overflow-hidden rounded-xl border shadow-sm">
|
|
842
|
+
<header className="border-border flex items-center justify-between border-b px-4 py-3">
|
|
843
|
+
<div className="min-w-0">
|
|
844
|
+
<h2 className="text-foreground text-sm font-medium">Content Health</h2>
|
|
845
|
+
<p className="text-muted-foreground mt-0.5 text-xs">SEO & quality issues</p>
|
|
846
|
+
</div>
|
|
847
|
+
<button
|
|
848
|
+
className="text-brand text-xs font-medium hover:underline"
|
|
849
|
+
onClick={() => nav('/seo')}
|
|
850
|
+
>
|
|
851
|
+
Fix issues
|
|
852
|
+
</button>
|
|
853
|
+
</header>
|
|
854
|
+
<div className="px-4 pt-3.5 pb-2.5">
|
|
855
|
+
<div className="mb-1.5 flex items-baseline justify-between">
|
|
856
|
+
<p className="text-foreground text-2xl leading-none font-medium tracking-tight">
|
|
857
|
+
{contentHealth.score > 0 ? contentHealth.score : '—'}
|
|
858
|
+
{contentHealth.score > 0 && (
|
|
859
|
+
<span className="text-muted-foreground ml-0.5 text-sm font-normal">/100</span>
|
|
860
|
+
)}
|
|
861
|
+
</p>
|
|
862
|
+
<span
|
|
863
|
+
className={`text-xs font-medium ${
|
|
864
|
+
contentHealth.tone === 'ok'
|
|
865
|
+
? 'text-success'
|
|
866
|
+
: contentHealth.tone === 'warn'
|
|
867
|
+
? 'text-warning'
|
|
868
|
+
: contentHealth.tone === 'err'
|
|
879
869
|
? 'text-destructive dark:text-red-400'
|
|
880
|
-
:
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
870
|
+
: 'text-muted-foreground'
|
|
871
|
+
}`}
|
|
872
|
+
>
|
|
873
|
+
{contentHealth.label}
|
|
874
|
+
</span>
|
|
875
|
+
</div>
|
|
876
|
+
<div className="bg-muted h-1.5 overflow-hidden rounded-full">
|
|
877
|
+
<div
|
|
878
|
+
className={`h-full rounded-full transition-all ${
|
|
879
|
+
contentHealth.tone === 'ok'
|
|
880
|
+
? 'bg-success'
|
|
881
|
+
: contentHealth.tone === 'warn'
|
|
882
|
+
? 'bg-warning'
|
|
883
|
+
: contentHealth.tone === 'err'
|
|
884
|
+
? 'bg-destructive'
|
|
885
|
+
: 'bg-muted-foreground/40'
|
|
886
|
+
}`}
|
|
887
|
+
style={{ width: `${Math.max(0, Math.min(100, contentHealth.score))}%` }}
|
|
888
|
+
/>
|
|
889
|
+
</div>
|
|
890
|
+
</div>
|
|
891
|
+
{contentHealth.issues.length === 0 ? (
|
|
892
|
+
<div className="text-muted-foreground px-4 pt-1 pb-4 text-xs">
|
|
893
|
+
No outstanding issues. Nice work.
|
|
894
|
+
</div>
|
|
895
|
+
) : (
|
|
896
|
+
<ul role="list" className="divide-border divide-y">
|
|
897
|
+
{contentHealth.issues.map((iss, i) => (
|
|
898
|
+
<li key={i}>
|
|
899
|
+
<button
|
|
900
|
+
type="button"
|
|
901
|
+
onClick={() => nav('/seo')}
|
|
902
|
+
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"
|
|
884
903
|
>
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
904
|
+
<span
|
|
905
|
+
className={`h-1.5 w-1.5 shrink-0 rounded-full ${
|
|
906
|
+
iss.tone === 'err'
|
|
907
|
+
? 'bg-destructive'
|
|
908
|
+
: iss.tone === 'warn'
|
|
909
|
+
? 'bg-warning'
|
|
910
|
+
: 'bg-muted-foreground'
|
|
911
|
+
}`}
|
|
912
|
+
aria-hidden
|
|
913
|
+
/>
|
|
914
|
+
<span className="text-muted-foreground flex-1 truncate text-xs">
|
|
915
|
+
{iss.label}
|
|
916
|
+
</span>
|
|
917
|
+
<span
|
|
918
|
+
className={`shrink-0 text-sm font-medium ${
|
|
919
|
+
iss.tone === 'err'
|
|
920
|
+
? 'text-destructive dark:text-red-400'
|
|
921
|
+
: iss.tone === 'warn'
|
|
922
|
+
? 'text-warning'
|
|
923
|
+
: 'text-muted-foreground'
|
|
924
|
+
}`}
|
|
925
|
+
>
|
|
926
|
+
{iss.count}
|
|
927
|
+
</span>
|
|
928
|
+
<ChevronRight className="text-muted-foreground/60 h-3.5 w-3.5 shrink-0" />
|
|
929
|
+
</button>
|
|
930
|
+
</li>
|
|
931
|
+
))}
|
|
932
|
+
</ul>
|
|
933
|
+
)}
|
|
934
|
+
</section>
|
|
935
|
+
</aside>
|
|
936
|
+
</div>
|
|
937
|
+
)}
|
|
896
938
|
|
|
897
939
|
{/* Delivery API ──────────────────────────────────────────────────
|
|
898
940
|
Headless content delivery metrics aggregated over the last
|