@actuate-media/cms-admin 0.65.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 +39 -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/RowActions.d.ts +19 -0
- package/dist/components/ui/RowActions.d.ts.map +1 -0
- package/dist/components/ui/RowActions.js +16 -0
- package/dist/components/ui/RowActions.js.map +1 -0
- package/dist/components/ui/index.d.ts +4 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +2 -0
- package/dist/components/ui/index.js.map +1 -1
- package/dist/layout/Header.js +1 -1
- package/dist/layout/Header.js.map +1 -1
- package/dist/layout/Layout.js +8 -38
- package/dist/layout/Layout.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-service.d.ts +9 -0
- package/dist/lib/pages-service.d.ts.map +1 -1
- package/dist/lib/pages-service.js +14 -0
- package/dist/lib/pages-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/lib/posts-service.d.ts +11 -0
- package/dist/lib/posts-service.d.ts.map +1 -1
- package/dist/lib/posts-service.js +13 -0
- package/dist/lib/posts-service.js.map +1 -1
- package/dist/lib/seo-service.d.ts +3 -0
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +40 -3
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +55 -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 +145 -16
- 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/Posts/PostsListView.d.ts.map +1 -1
- package/dist/views/Posts/PostsListView.js +66 -9
- package/dist/views/Posts/PostsListView.js.map +1 -1
- 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/dist/views/seo/TechnicalTab.d.ts.map +1 -1
- package/dist/views/seo/TechnicalTab.js +15 -0
- package/dist/views/seo/TechnicalTab.js.map +1 -1
- package/package.json +3 -3
- 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/RowActions.tsx +50 -0
- package/src/components/ui/index.ts +12 -0
- package/src/layout/Header.tsx +2 -2
- package/src/layout/Layout.tsx +8 -80
- package/src/lib/page-editor-service.ts +16 -2
- package/src/lib/pages-service.ts +18 -0
- package/src/lib/pages-tree.ts +145 -0
- package/src/lib/posts-service.ts +26 -0
- package/src/lib/seo-service.ts +3 -0
- package/src/views/Dashboard.tsx +286 -214
- package/src/views/MediaBrowser.tsx +673 -512
- package/src/views/MediaSeoAudit.tsx +320 -0
- package/src/views/Pages/PagesListView.tsx +459 -185
- package/src/views/Pages/PagesTreeTable.tsx +339 -0
- package/src/views/Posts/PostsListView.tsx +135 -10
- 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/src/views/seo/TechnicalTab.tsx +15 -0
- 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
|
|
@@ -287,6 +289,14 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
287
289
|
|
|
288
290
|
const collections = useMemo(() => resolveCollections(config), [config])
|
|
289
291
|
|
|
292
|
+
// Hero CTA targets (mirrors the quick-action "New Post" resolution).
|
|
293
|
+
const heroNewPostPath = useMemo(() => {
|
|
294
|
+
const postTypes = collections.filter((c) => c.type === 'post')
|
|
295
|
+
if (postTypes.length === 1) return `/${postTypes[0]!.slug}/new`
|
|
296
|
+
return postTypes.length > 1 ? '/posts/new' : '/posts/types/new'
|
|
297
|
+
}, [collections])
|
|
298
|
+
const siteUrl: string | null = config?.site?.url ?? config?.seo?.siteUrl ?? null
|
|
299
|
+
|
|
290
300
|
// ── Stat cards ──────────────────────────────────────────────────────────
|
|
291
301
|
const statCards: StatCardData[] = useMemo(() => {
|
|
292
302
|
const counts = stats?.collectionCounts ?? {}
|
|
@@ -513,6 +523,40 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
513
523
|
|
|
514
524
|
const totalIssues = contentHealth.issues.reduce((s, i) => s + i.count, 0)
|
|
515
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
|
+
|
|
516
560
|
// ── Render ──────────────────────────────────────────────────────────────
|
|
517
561
|
|
|
518
562
|
if (loading && !stats) {
|
|
@@ -561,24 +605,46 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
561
605
|
)}
|
|
562
606
|
|
|
563
607
|
{/* Hero greeting ─────────────────────────────────────────────────
|
|
564
|
-
|
|
565
|
-
(
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
608
|
+
Per the design, the primary "+ New Post" and "View Site" actions
|
|
609
|
+
sit on the right of the hero (`.dash-hdr` → `.dh-r`), not in the
|
|
610
|
+
top bar. The quick-action row below keeps the wider authoring
|
|
611
|
+
shortcuts. */}
|
|
612
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
613
|
+
<div className="flex flex-col gap-1 sm:gap-0.5">
|
|
614
|
+
<h1 className="text-foreground">
|
|
615
|
+
{greeting}, {userName} <span aria-hidden>👋</span>
|
|
616
|
+
</h1>
|
|
617
|
+
<p className="text-muted-foreground text-sm">
|
|
618
|
+
{dateStr}
|
|
619
|
+
{totalIssues > 0 && (
|
|
620
|
+
<span className="hidden sm:inline">
|
|
621
|
+
{' · '}
|
|
622
|
+
{totalIssues} content item{totalIssues === 1 ? '' : 's'} need attention
|
|
623
|
+
</span>
|
|
624
|
+
)}
|
|
625
|
+
</p>
|
|
626
|
+
</div>
|
|
627
|
+
<div className="flex items-center gap-2">
|
|
628
|
+
<button
|
|
629
|
+
type="button"
|
|
630
|
+
onClick={() => nav(heroNewPostPath)}
|
|
631
|
+
className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium shadow-sm transition-colors focus:outline-none focus-visible:ring-2"
|
|
632
|
+
>
|
|
633
|
+
<Plus className="h-3.5 w-3.5" aria-hidden />
|
|
634
|
+
New Post
|
|
635
|
+
</button>
|
|
636
|
+
{siteUrl ? (
|
|
637
|
+
<a
|
|
638
|
+
href={siteUrl}
|
|
639
|
+
target="_blank"
|
|
640
|
+
rel="noopener noreferrer"
|
|
641
|
+
className="border-border bg-card text-foreground hover:bg-accent inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition-colors"
|
|
642
|
+
>
|
|
643
|
+
<ExternalLink className="h-3.5 w-3.5" aria-hidden />
|
|
644
|
+
View Site
|
|
645
|
+
</a>
|
|
646
|
+
) : null}
|
|
647
|
+
</div>
|
|
582
648
|
</div>
|
|
583
649
|
|
|
584
650
|
{/* Quick actions ──────────────────────────────────────────────── */}
|
|
@@ -641,124 +707,71 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
641
707
|
})}
|
|
642
708
|
</div>
|
|
643
709
|
|
|
644
|
-
{/*
|
|
645
|
-
|
|
646
|
-
{
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
<h2 className="text-foreground text-sm font-medium">Recent Activity</h2>
|
|
651
|
-
<p className="text-muted-foreground mt-0.5 text-xs">Last 7 days across all content</p>
|
|
652
|
-
</div>
|
|
653
|
-
{(stats?.recentDocuments?.length ?? 0) > 8 && (
|
|
654
|
-
<button
|
|
655
|
-
className="text-brand text-xs font-medium hover:underline"
|
|
656
|
-
onClick={() => setActivityLimit((n) => (n >= 20 ? 8 : 20))}
|
|
657
|
-
>
|
|
658
|
-
{activityLimit >= 20 ? 'Show less' : 'Show more'}
|
|
659
|
-
</button>
|
|
660
|
-
)}
|
|
661
|
-
</header>
|
|
662
|
-
{activity.length === 0 ? (
|
|
663
|
-
<EmptyState
|
|
664
|
-
icon={Activity}
|
|
665
|
-
title="No activity yet"
|
|
666
|
-
subtitle="Content changes will appear here as your team works."
|
|
667
|
-
/>
|
|
668
|
-
) : (
|
|
669
|
-
<ul role="list" className="divide-border divide-y">
|
|
670
|
-
{activity.map((it) => (
|
|
671
|
-
<li key={it.id}>
|
|
672
|
-
<button
|
|
673
|
-
type="button"
|
|
674
|
-
onClick={() => nav(it.editPath)}
|
|
675
|
-
className="hover:bg-accent/50 focus-visible:bg-accent/50 w-full cursor-pointer px-4 py-3 text-left transition-colors"
|
|
676
|
-
>
|
|
677
|
-
<div className="flex min-w-0 items-start gap-3">
|
|
678
|
-
<div
|
|
679
|
-
className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-medium text-white"
|
|
680
|
-
style={{ background: it.avatar.color }}
|
|
681
|
-
aria-hidden
|
|
682
|
-
>
|
|
683
|
-
{it.avatar.initials}
|
|
684
|
-
</div>
|
|
685
|
-
<div className="min-w-0 flex-1">
|
|
686
|
-
<p className="text-foreground truncate text-sm leading-snug">
|
|
687
|
-
<span className="font-medium">
|
|
688
|
-
“{it.title || 'Untitled'}”
|
|
689
|
-
</span>{' '}
|
|
690
|
-
<span className="text-muted-foreground">— {it.typeLabel}</span>
|
|
691
|
-
</p>
|
|
692
|
-
<div className="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-[11px]">
|
|
693
|
-
<span
|
|
694
|
-
className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${it.statusInfo.cls}`}
|
|
695
|
-
>
|
|
696
|
-
{it.statusInfo.label}
|
|
697
|
-
</span>
|
|
698
|
-
<span className="max-w-[120px] truncate">{it.author}</span>
|
|
699
|
-
<span aria-hidden>·</span>
|
|
700
|
-
<span>{it.relTime}</span>
|
|
701
|
-
</div>
|
|
702
|
-
</div>
|
|
703
|
-
</div>
|
|
704
|
-
</button>
|
|
705
|
-
</li>
|
|
706
|
-
))}
|
|
707
|
-
</ul>
|
|
708
|
-
)}
|
|
709
|
-
</section>
|
|
710
|
-
|
|
711
|
-
{/* Right column ─────────────────────────────────────────── */}
|
|
712
|
-
<aside className="flex min-w-0 flex-col gap-4">
|
|
713
|
-
{/* 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 ───────────────────────────────────────── */}
|
|
714
716
|
<section className="bg-card border-border overflow-hidden rounded-xl border shadow-sm">
|
|
715
717
|
<header className="border-border flex items-center justify-between border-b px-4 py-3">
|
|
716
718
|
<div className="min-w-0">
|
|
717
|
-
<h2 className="text-foreground text-sm font-medium">
|
|
719
|
+
<h2 className="text-foreground text-sm font-medium">Recent Activity</h2>
|
|
718
720
|
<p className="text-muted-foreground mt-0.5 text-xs">
|
|
719
|
-
|
|
721
|
+
Last 7 days across all content
|
|
720
722
|
</p>
|
|
721
723
|
</div>
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
if (first) nav(first.editPath)
|
|
731
|
-
}}
|
|
732
|
-
>
|
|
733
|
-
Manage
|
|
734
|
-
</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
|
+
)}
|
|
735
732
|
</header>
|
|
736
|
-
{
|
|
733
|
+
{activity.length === 0 ? (
|
|
737
734
|
<EmptyState
|
|
738
|
-
icon={
|
|
739
|
-
title="No
|
|
740
|
-
subtitle="
|
|
741
|
-
compact
|
|
735
|
+
icon={Activity}
|
|
736
|
+
title="No activity yet"
|
|
737
|
+
subtitle="Content changes will appear here as your team works."
|
|
742
738
|
/>
|
|
743
739
|
) : (
|
|
744
740
|
<ul role="list" className="divide-border divide-y">
|
|
745
|
-
{
|
|
746
|
-
<li key={
|
|
741
|
+
{activity.map((it) => (
|
|
742
|
+
<li key={it.id}>
|
|
747
743
|
<button
|
|
748
744
|
type="button"
|
|
749
|
-
onClick={() => nav(
|
|
750
|
-
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"
|
|
751
747
|
>
|
|
752
|
-
<
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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>
|
|
758
774
|
</div>
|
|
759
|
-
<span className="border-border bg-background text-muted-foreground shrink-0 rounded border px-1.5 py-0.5 text-[10px] capitalize">
|
|
760
|
-
{q.type}
|
|
761
|
-
</span>
|
|
762
775
|
</button>
|
|
763
776
|
</li>
|
|
764
777
|
))}
|
|
@@ -766,103 +779,162 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
766
779
|
)}
|
|
767
780
|
</section>
|
|
768
781
|
|
|
769
|
-
{/*
|
|
770
|
-
<
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
<
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
</p>
|
|
791
|
-
<span
|
|
792
|
-
className={`text-xs font-medium ${
|
|
793
|
-
contentHealth.tone === 'ok'
|
|
794
|
-
? 'text-success'
|
|
795
|
-
: contentHealth.tone === 'warn'
|
|
796
|
-
? 'text-warning'
|
|
797
|
-
: contentHealth.tone === 'err'
|
|
798
|
-
? 'text-destructive dark:text-red-400'
|
|
799
|
-
: 'text-muted-foreground'
|
|
800
|
-
}`}
|
|
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
|
+
}}
|
|
801
803
|
>
|
|
802
|
-
|
|
803
|
-
</
|
|
804
|
-
</
|
|
805
|
-
|
|
806
|
-
<
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
? 'bg-warning'
|
|
812
|
-
: contentHealth.tone === 'err'
|
|
813
|
-
? 'bg-destructive'
|
|
814
|
-
: 'bg-muted-foreground/40'
|
|
815
|
-
}`}
|
|
816
|
-
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
|
|
817
813
|
/>
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
className=
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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'
|
|
849
869
|
? 'text-destructive dark:text-red-400'
|
|
850
|
-
:
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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"
|
|
854
903
|
>
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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
|
+
)}
|
|
866
938
|
|
|
867
939
|
{/* Delivery API ──────────────────────────────────────────────────
|
|
868
940
|
Headless content delivery metrics aggregated over the last
|