@actuate-media/cms-admin 0.16.0 → 0.17.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.d.ts.map +1 -1
- package/dist/AdminRoot.js +1 -1
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/lib/useApiData.test.d.ts +2 -0
- package/dist/__tests__/lib/useApiData.test.d.ts.map +1 -0
- package/dist/__tests__/lib/useApiData.test.js +111 -0
- package/dist/__tests__/lib/useApiData.test.js.map +1 -0
- package/dist/__tests__/views/dashboard.test.d.ts +2 -0
- package/dist/__tests__/views/dashboard.test.d.ts.map +1 -0
- package/dist/__tests__/views/dashboard.test.js +138 -0
- package/dist/__tests__/views/dashboard.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/lib/useApiData.d.ts +11 -0
- package/dist/lib/useApiData.d.ts.map +1 -1
- package/dist/lib/useApiData.js +41 -7
- package/dist/lib/useApiData.js.map +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +99 -60
- package/dist/views/Dashboard.js.map +1 -1
- package/package.json +2 -2
- package/src/AdminRoot.tsx +1 -0
- package/src/__tests__/lib/useApiData.test.ts +140 -0
- package/src/__tests__/views/dashboard.test.tsx +180 -0
- package/src/lib/useApiData.ts +52 -7
- package/src/styles/theme.css +37 -0
- package/src/views/Dashboard.tsx +201 -144
package/src/views/Dashboard.tsx
CHANGED
|
@@ -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
|
|
@@ -199,33 +217,32 @@ function authorAvatar(name: string | null | undefined): { color: string; initial
|
|
|
199
217
|
return { color, initials }
|
|
200
218
|
}
|
|
201
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Semantic state styling uses theme tokens (`success` / `warning` / `info`
|
|
222
|
+
* / `brand`) so the dashboard tracks the design system in both light and
|
|
223
|
+
* dark mode. Two deliberate exceptions remain as raw values:
|
|
224
|
+
* - Error states keep `text-red-*` / `bg-red-*`. The `--destructive`
|
|
225
|
+
* token's dark value is intentionally dark (low contrast as TEXT on a
|
|
226
|
+
* dark card), so tokenizing error text here would regress contrast.
|
|
227
|
+
* Promoting error to a token is a separate, admin-wide change.
|
|
228
|
+
* - `AVATAR_COLORS` are dynamic per-author hashes (allowed: truly
|
|
229
|
+
* dynamic values, applied via inline style).
|
|
230
|
+
*/
|
|
202
231
|
function statusBadge(status: string): {
|
|
203
232
|
label: string
|
|
204
233
|
cls: string
|
|
205
234
|
} {
|
|
206
235
|
switch (status) {
|
|
207
236
|
case 'PUBLISHED':
|
|
208
|
-
return {
|
|
209
|
-
label: 'Published',
|
|
210
|
-
cls: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950/60 dark:text-emerald-300',
|
|
211
|
-
}
|
|
237
|
+
return { label: 'Published', cls: 'bg-success/10 text-success' }
|
|
212
238
|
case 'DRAFT':
|
|
213
|
-
return {
|
|
214
|
-
label: 'Draft',
|
|
215
|
-
cls: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300',
|
|
216
|
-
}
|
|
239
|
+
return { label: 'Draft', cls: 'bg-muted text-muted-foreground' }
|
|
217
240
|
case 'SCHEDULED':
|
|
218
|
-
return {
|
|
219
|
-
label: 'Scheduled',
|
|
220
|
-
cls: 'bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-300',
|
|
221
|
-
}
|
|
241
|
+
return { label: 'Scheduled', cls: 'bg-warning/10 text-warning' }
|
|
222
242
|
case 'IN_REVIEW':
|
|
223
|
-
return {
|
|
224
|
-
label: 'In Review',
|
|
225
|
-
cls: 'bg-blue-100 text-blue-800 dark:bg-blue-950/60 dark:text-blue-300',
|
|
226
|
-
}
|
|
243
|
+
return { label: 'In Review', cls: 'bg-info/10 text-info' }
|
|
227
244
|
default:
|
|
228
|
-
return { label: status, cls: 'bg-
|
|
245
|
+
return { label: status, cls: 'bg-muted text-muted-foreground' }
|
|
229
246
|
}
|
|
230
247
|
}
|
|
231
248
|
|
|
@@ -245,15 +262,44 @@ interface StatCardData {
|
|
|
245
262
|
|
|
246
263
|
export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
247
264
|
const nav = (path: string) => onNavigate?.(path)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
265
|
+
// Cache responses for 30s so navigating away and back doesn't re-fire
|
|
266
|
+
// all four dashboard requests on every visit. First load is unaffected
|
|
267
|
+
// (cold cache) and each request still streams independently; `refetch`
|
|
268
|
+
// (the Retry button) bypasses the cache.
|
|
269
|
+
const DASH_CACHE_MS = 30_000
|
|
270
|
+
const {
|
|
271
|
+
data: stats,
|
|
272
|
+
loading,
|
|
273
|
+
error,
|
|
274
|
+
exhausted,
|
|
275
|
+
refetch,
|
|
276
|
+
} = useApiData<DashboardStats>('/stats', null, {
|
|
277
|
+
cacheMs: DASH_CACHE_MS,
|
|
278
|
+
})
|
|
279
|
+
const { data: health } = useApiData<HealthData>('/health', null, { cacheMs: DASH_CACHE_MS })
|
|
280
|
+
const { data: delivery, loading: deliveryLoading } = useApiData<DeliveryStats>(
|
|
281
|
+
'/stats/api-delivery',
|
|
282
|
+
null,
|
|
283
|
+
{ cacheMs: DASH_CACHE_MS },
|
|
284
|
+
)
|
|
285
|
+
const { data: contentHealthData, loading: contentHealthLoading } = useApiData<ContentHealthData>(
|
|
286
|
+
'/stats/content-health',
|
|
287
|
+
null,
|
|
288
|
+
{ cacheMs: DASH_CACHE_MS },
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
// A slow ticker (60s) so the greeting, calendar date, and "x minutes
|
|
292
|
+
// ago" relative times stay correct on a dashboard left open for hours
|
|
293
|
+
// — without it, a tab opened at 11:58am would read "Good morning" all
|
|
294
|
+
// afternoon and show stale activity timestamps.
|
|
295
|
+
const [now, setNow] = useState(() => Date.now())
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
const id = setInterval(() => setNow(Date.now()), 60_000)
|
|
298
|
+
return () => clearInterval(id)
|
|
299
|
+
}, [])
|
|
300
|
+
|
|
301
|
+
const greeting = useMemo(() => timeOfDayGreeting(), [now])
|
|
302
|
+
const dateStr = useMemo(() => todayDateString(), [now])
|
|
257
303
|
const userName = useMemo(() => firstNameForGreeting(session), [session])
|
|
258
304
|
|
|
259
305
|
const collections = useMemo(() => resolveCollections(config), [config])
|
|
@@ -283,8 +329,8 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
283
329
|
label: collectionLabel(primary[0]),
|
|
284
330
|
value: String(counts[primary[0].slug] ?? 0),
|
|
285
331
|
icon: primary[0].type === 'page' ? FileIcon : FileText,
|
|
286
|
-
iconBg: 'bg-
|
|
287
|
-
iconColor: 'text-
|
|
332
|
+
iconBg: 'bg-chart-1/15',
|
|
333
|
+
iconColor: 'text-chart-1',
|
|
288
334
|
href: `/${primary[0].slug}`,
|
|
289
335
|
})
|
|
290
336
|
} else {
|
|
@@ -292,8 +338,8 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
292
338
|
label: 'Documents',
|
|
293
339
|
value: String(stats?.totalDocuments ?? 0),
|
|
294
340
|
icon: FileText,
|
|
295
|
-
iconBg: 'bg-
|
|
296
|
-
iconColor: 'text-
|
|
341
|
+
iconBg: 'bg-chart-1/15',
|
|
342
|
+
iconColor: 'text-chart-1',
|
|
297
343
|
})
|
|
298
344
|
}
|
|
299
345
|
|
|
@@ -302,8 +348,8 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
302
348
|
label: collectionLabel(primary[1]),
|
|
303
349
|
value: String(counts[primary[1].slug] ?? 0),
|
|
304
350
|
icon: primary[1].type === 'page' ? FileIcon : FileText,
|
|
305
|
-
iconBg: 'bg-
|
|
306
|
-
iconColor: 'text-
|
|
351
|
+
iconBg: 'bg-chart-2/15',
|
|
352
|
+
iconColor: 'text-chart-2',
|
|
307
353
|
href: `/${primary[1].slug}`,
|
|
308
354
|
})
|
|
309
355
|
}
|
|
@@ -316,8 +362,8 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
316
362
|
? `${stats.totalMedia} file${stats.totalMedia === 1 ? '' : 's'}`
|
|
317
363
|
: undefined,
|
|
318
364
|
icon: ImageIcon,
|
|
319
|
-
iconBg: 'bg-
|
|
320
|
-
iconColor: 'text-
|
|
365
|
+
iconBg: 'bg-chart-3/15',
|
|
366
|
+
iconColor: 'text-chart-3',
|
|
321
367
|
href: '/media',
|
|
322
368
|
})
|
|
323
369
|
|
|
@@ -330,8 +376,8 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
330
376
|
value: String(formsCount),
|
|
331
377
|
hint: submissions > 0 ? `${submissions} response${submissions === 1 ? '' : 's'}` : undefined,
|
|
332
378
|
icon: ClipboardList,
|
|
333
|
-
iconBg: 'bg-
|
|
334
|
-
iconColor: 'text-
|
|
379
|
+
iconBg: 'bg-chart-4/15',
|
|
380
|
+
iconColor: 'text-chart-4',
|
|
335
381
|
href: '/forms',
|
|
336
382
|
})
|
|
337
383
|
|
|
@@ -343,8 +389,8 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
343
389
|
hint: seo > 0 ? (seo >= 70 ? 'Good' : seo >= 40 ? 'Fair' : 'Needs work') : 'No content yet',
|
|
344
390
|
hintUp: seo >= 70,
|
|
345
391
|
icon: Search,
|
|
346
|
-
iconBg: 'bg-
|
|
347
|
-
iconColor: 'text-
|
|
392
|
+
iconBg: 'bg-chart-5/15',
|
|
393
|
+
iconColor: 'text-chart-5',
|
|
348
394
|
href: '/seo',
|
|
349
395
|
})
|
|
350
396
|
|
|
@@ -396,9 +442,12 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
396
442
|
relTime: relativeTime(d.updatedAt),
|
|
397
443
|
avatar: authorAvatar(d.author),
|
|
398
444
|
statusInfo: statusBadge(d.status),
|
|
445
|
+
editPath: editPathForDoc(col, d.collection, d.id),
|
|
399
446
|
}
|
|
400
447
|
})
|
|
401
|
-
|
|
448
|
+
// `now` keeps the relative timestamps fresh on a long-open tab.
|
|
449
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
450
|
+
}, [stats, collections, activityLimit, now])
|
|
402
451
|
|
|
403
452
|
// ── Publishing queue (scheduled docs only) ──────────────────────────────
|
|
404
453
|
const publishQueue = useMemo(() => {
|
|
@@ -415,9 +464,11 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
415
464
|
type: col ? collectionLabel(col, false) : d.collection,
|
|
416
465
|
date: relativeTime(d.updatedAt),
|
|
417
466
|
author: d.author,
|
|
467
|
+
editPath: editPathForDoc(col, d.collection, d.id),
|
|
418
468
|
}
|
|
419
469
|
})
|
|
420
|
-
|
|
470
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
471
|
+
}, [stats, collections, now])
|
|
421
472
|
|
|
422
473
|
// ── Content health (sourced from /stats/content-health) ─────────────────
|
|
423
474
|
// Persisted by the nightly content-health cron. The four issue rows
|
|
@@ -484,7 +535,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
484
535
|
if (loading && !stats) {
|
|
485
536
|
return (
|
|
486
537
|
<div className="flex h-64 items-center justify-center p-4 sm:p-6">
|
|
487
|
-
<Loader2 className="h-6 w-6 animate-spin
|
|
538
|
+
<Loader2 className="text-brand h-6 w-6 animate-spin" />
|
|
488
539
|
</div>
|
|
489
540
|
)
|
|
490
541
|
}
|
|
@@ -493,13 +544,11 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
493
544
|
<div className="w-full space-y-5 p-4 sm:p-6 lg:px-8">
|
|
494
545
|
{/* Health banners ────────────────────────────────────────────── */}
|
|
495
546
|
{health && health.status === 'degraded' && (
|
|
496
|
-
<div className="flex items-start gap-3 rounded-lg border
|
|
497
|
-
<Database className="mt-0.5 h-5 w-5 shrink-0
|
|
547
|
+
<div className="border-info/30 bg-info/10 flex items-start gap-3 rounded-lg border p-3">
|
|
548
|
+
<Database className="text-info mt-0.5 h-5 w-5 shrink-0" />
|
|
498
549
|
<div className="min-w-0 flex-1">
|
|
499
|
-
<span className="text-sm font-medium
|
|
500
|
-
|
|
501
|
-
</span>
|
|
502
|
-
<p className="mt-0.5 text-xs text-blue-700 dark:text-blue-300">
|
|
550
|
+
<span className="text-info text-sm font-medium">Database setup required</span>
|
|
551
|
+
<p className="text-muted-foreground mt-0.5 text-xs">
|
|
503
552
|
{!health.databaseConnected
|
|
504
553
|
? 'Cannot connect to the database. Check your DATABASE_URL environment variable.'
|
|
505
554
|
: !health.secretConfigured
|
|
@@ -514,14 +563,14 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
514
563
|
)}
|
|
515
564
|
|
|
516
565
|
{error && exhausted && (
|
|
517
|
-
<div className="flex items-center gap-3 rounded-lg border
|
|
518
|
-
<AlertTriangle className="h-5 w-5 shrink-0
|
|
519
|
-
<span className="flex-1 text-sm
|
|
566
|
+
<div className="border-warning/30 bg-warning/10 flex items-center gap-3 rounded-lg border p-3">
|
|
567
|
+
<AlertTriangle className="text-warning h-5 w-5 shrink-0" />
|
|
568
|
+
<span className="text-warning flex-1 text-sm">
|
|
520
569
|
Some dashboard data may be unavailable.
|
|
521
570
|
</span>
|
|
522
571
|
<button
|
|
523
572
|
onClick={refetch}
|
|
524
|
-
className="rounded-lg border
|
|
573
|
+
className="border-warning/40 text-warning hover:bg-warning/10 rounded-lg border px-3 py-1 text-sm transition-colors"
|
|
525
574
|
>
|
|
526
575
|
Retry
|
|
527
576
|
</button>
|
|
@@ -555,7 +604,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
555
604
|
<button
|
|
556
605
|
key={a.label}
|
|
557
606
|
onClick={a.onClick}
|
|
558
|
-
className="border-border bg-card inline-flex shrink-0 items-center gap-1.5 rounded-lg border px-3.5 py-2 text-sm shadow-sm transition-colors
|
|
607
|
+
className="border-border bg-card hover:border-brand/50 hover:bg-brand/10 hover:text-brand inline-flex shrink-0 items-center gap-1.5 rounded-lg border px-3.5 py-2 text-sm shadow-sm transition-colors"
|
|
559
608
|
>
|
|
560
609
|
<a.icon className="h-3.5 w-3.5" />
|
|
561
610
|
{a.label}
|
|
@@ -584,7 +633,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
584
633
|
<p className="text-muted-foreground mt-1 text-xs">{card.label}</p>
|
|
585
634
|
{card.hint && (
|
|
586
635
|
<p
|
|
587
|
-
className={`mt-1.5 text-[11px] ${card.hintUp ? 'text-
|
|
636
|
+
className={`mt-1.5 text-[11px] ${card.hintUp ? 'text-success' : 'text-muted-foreground'}`}
|
|
588
637
|
>
|
|
589
638
|
{card.hint}
|
|
590
639
|
</p>
|
|
@@ -618,12 +667,14 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
618
667
|
<h2 className="text-foreground text-sm font-semibold">Recent Activity</h2>
|
|
619
668
|
<p className="text-muted-foreground mt-0.5 text-xs">Last 7 days across all content</p>
|
|
620
669
|
</div>
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
670
|
+
{(stats?.recentDocuments?.length ?? 0) > 8 && (
|
|
671
|
+
<button
|
|
672
|
+
className="text-brand text-xs font-medium hover:underline"
|
|
673
|
+
onClick={() => setActivityLimit((n) => (n >= 20 ? 8 : 20))}
|
|
674
|
+
>
|
|
675
|
+
{activityLimit >= 20 ? 'Show less' : 'Show more'}
|
|
676
|
+
</button>
|
|
677
|
+
)}
|
|
627
678
|
</header>
|
|
628
679
|
{activity.length === 0 ? (
|
|
629
680
|
<EmptyState
|
|
@@ -634,38 +685,40 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
634
685
|
) : (
|
|
635
686
|
<ul role="list" className="divide-border divide-y">
|
|
636
687
|
{activity.map((it) => (
|
|
637
|
-
<li
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
<div
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
<
|
|
652
|
-
<
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
<
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
688
|
+
<li key={it.id}>
|
|
689
|
+
<button
|
|
690
|
+
type="button"
|
|
691
|
+
onClick={() => nav(it.editPath)}
|
|
692
|
+
className="hover:bg-accent/50 focus-visible:bg-accent/50 w-full cursor-pointer px-4 py-3 text-left transition-colors"
|
|
693
|
+
>
|
|
694
|
+
<div className="flex min-w-0 items-start gap-3">
|
|
695
|
+
<div
|
|
696
|
+
className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-bold text-white"
|
|
697
|
+
style={{ background: it.avatar.color }}
|
|
698
|
+
aria-hidden
|
|
699
|
+
>
|
|
700
|
+
{it.avatar.initials}
|
|
701
|
+
</div>
|
|
702
|
+
<div className="min-w-0 flex-1">
|
|
703
|
+
<p className="text-foreground truncate text-sm leading-snug">
|
|
704
|
+
<span className="font-semibold">
|
|
705
|
+
“{it.title || 'Untitled'}”
|
|
706
|
+
</span>{' '}
|
|
707
|
+
<span className="text-muted-foreground">— {it.typeLabel}</span>
|
|
708
|
+
</p>
|
|
709
|
+
<div className="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-[11px]">
|
|
710
|
+
<span
|
|
711
|
+
className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${it.statusInfo.cls}`}
|
|
712
|
+
>
|
|
713
|
+
{it.statusInfo.label}
|
|
714
|
+
</span>
|
|
715
|
+
<span className="max-w-[120px] truncate">{it.author}</span>
|
|
716
|
+
<span aria-hidden>·</span>
|
|
717
|
+
<span>{it.relTime}</span>
|
|
718
|
+
</div>
|
|
666
719
|
</div>
|
|
667
720
|
</div>
|
|
668
|
-
</
|
|
721
|
+
</button>
|
|
669
722
|
</li>
|
|
670
723
|
))}
|
|
671
724
|
</ul>
|
|
@@ -684,14 +737,14 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
684
737
|
</p>
|
|
685
738
|
</div>
|
|
686
739
|
<button
|
|
687
|
-
className="text-xs font-medium
|
|
740
|
+
className="text-brand text-xs font-medium hover:underline disabled:cursor-default disabled:no-underline disabled:opacity-50"
|
|
688
741
|
disabled={publishQueue.length === 0}
|
|
689
742
|
onClick={() => {
|
|
690
743
|
// We don't have a dedicated "scheduled" admin page, so deep-
|
|
691
744
|
// link to the first scheduled item — that's where authors
|
|
692
745
|
// typically need to land to reschedule or cancel.
|
|
693
746
|
const first = publishQueue[0]
|
|
694
|
-
if (first) nav(
|
|
747
|
+
if (first) nav(first.editPath)
|
|
695
748
|
}}
|
|
696
749
|
>
|
|
697
750
|
Manage
|
|
@@ -707,21 +760,23 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
707
760
|
) : (
|
|
708
761
|
<ul role="list" className="divide-border divide-y">
|
|
709
762
|
{publishQueue.map((q) => (
|
|
710
|
-
<li
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
<
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
763
|
+
<li key={q.id}>
|
|
764
|
+
<button
|
|
765
|
+
type="button"
|
|
766
|
+
onClick={() => nav(q.editPath)}
|
|
767
|
+
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"
|
|
768
|
+
>
|
|
769
|
+
<span className="bg-warning h-2 w-2 shrink-0 rounded-full" aria-hidden />
|
|
770
|
+
<div className="min-w-0 flex-1">
|
|
771
|
+
<p className="text-foreground truncate text-sm font-medium">{q.title}</p>
|
|
772
|
+
<p className="text-muted-foreground truncate text-[11px]">
|
|
773
|
+
{q.date} · {q.author}
|
|
774
|
+
</p>
|
|
775
|
+
</div>
|
|
776
|
+
<span className="border-border bg-background text-muted-foreground shrink-0 rounded border px-1.5 py-0.5 text-[10px] capitalize">
|
|
777
|
+
{q.type}
|
|
778
|
+
</span>
|
|
779
|
+
</button>
|
|
725
780
|
</li>
|
|
726
781
|
))}
|
|
727
782
|
</ul>
|
|
@@ -736,7 +791,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
736
791
|
<p className="text-muted-foreground mt-0.5 text-xs">SEO & quality issues</p>
|
|
737
792
|
</div>
|
|
738
793
|
<button
|
|
739
|
-
className="text-xs font-medium
|
|
794
|
+
className="text-brand text-xs font-medium hover:underline"
|
|
740
795
|
onClick={() => nav('/seo')}
|
|
741
796
|
>
|
|
742
797
|
Fix issues
|
|
@@ -753,9 +808,9 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
753
808
|
<span
|
|
754
809
|
className={`text-xs font-medium ${
|
|
755
810
|
contentHealth.tone === 'ok'
|
|
756
|
-
? 'text-
|
|
811
|
+
? 'text-success'
|
|
757
812
|
: contentHealth.tone === 'warn'
|
|
758
|
-
? 'text-
|
|
813
|
+
? 'text-warning'
|
|
759
814
|
: contentHealth.tone === 'err'
|
|
760
815
|
? 'text-red-600 dark:text-red-400'
|
|
761
816
|
: 'text-muted-foreground'
|
|
@@ -768,9 +823,9 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
768
823
|
<div
|
|
769
824
|
className={`h-full rounded-full transition-all ${
|
|
770
825
|
contentHealth.tone === 'ok'
|
|
771
|
-
? 'bg-
|
|
826
|
+
? 'bg-success'
|
|
772
827
|
: contentHealth.tone === 'warn'
|
|
773
|
-
? 'bg-
|
|
828
|
+
? 'bg-warning'
|
|
774
829
|
: contentHealth.tone === 'err'
|
|
775
830
|
? 'bg-red-500'
|
|
776
831
|
: 'bg-muted-foreground/40'
|
|
@@ -786,36 +841,38 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
786
841
|
) : (
|
|
787
842
|
<ul role="list" className="divide-border divide-y">
|
|
788
843
|
{contentHealth.issues.map((iss, i) => (
|
|
789
|
-
<li
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
-
}`}
|
|
844
|
+
<li key={i}>
|
|
845
|
+
<button
|
|
846
|
+
type="button"
|
|
847
|
+
onClick={() => nav('/seo')}
|
|
848
|
+
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
849
|
>
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
850
|
+
<span
|
|
851
|
+
className={`h-1.5 w-1.5 shrink-0 rounded-full ${
|
|
852
|
+
iss.tone === 'err'
|
|
853
|
+
? 'bg-red-500'
|
|
854
|
+
: iss.tone === 'warn'
|
|
855
|
+
? 'bg-warning'
|
|
856
|
+
: 'bg-muted-foreground'
|
|
857
|
+
}`}
|
|
858
|
+
aria-hidden
|
|
859
|
+
/>
|
|
860
|
+
<span className="text-muted-foreground flex-1 truncate text-xs">
|
|
861
|
+
{iss.label}
|
|
862
|
+
</span>
|
|
863
|
+
<span
|
|
864
|
+
className={`shrink-0 text-sm font-semibold ${
|
|
865
|
+
iss.tone === 'err'
|
|
866
|
+
? 'text-red-600 dark:text-red-400'
|
|
867
|
+
: iss.tone === 'warn'
|
|
868
|
+
? 'text-warning'
|
|
869
|
+
: 'text-muted-foreground'
|
|
870
|
+
}`}
|
|
871
|
+
>
|
|
872
|
+
{iss.count}
|
|
873
|
+
</span>
|
|
874
|
+
<ChevronRight className="text-muted-foreground/60 h-3.5 w-3.5 shrink-0" />
|
|
875
|
+
</button>
|
|
819
876
|
</li>
|
|
820
877
|
))}
|
|
821
878
|
</ul>
|
|
@@ -841,7 +898,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
841
898
|
</p>
|
|
842
899
|
</div>
|
|
843
900
|
<button
|
|
844
|
-
className="inline-flex items-center gap-1 text-xs font-medium
|
|
901
|
+
className="text-brand inline-flex items-center gap-1 text-xs font-medium hover:underline"
|
|
845
902
|
onClick={() => nav('/api-keys')}
|
|
846
903
|
>
|
|
847
904
|
API docs <ExternalLink className="h-3 w-3" />
|
|
@@ -996,9 +1053,9 @@ function DeliveryTile({
|
|
|
996
1053
|
}) {
|
|
997
1054
|
const subTone =
|
|
998
1055
|
tone === 'ok'
|
|
999
|
-
? 'text-
|
|
1056
|
+
? 'text-success'
|
|
1000
1057
|
: tone === 'warn'
|
|
1001
|
-
? 'text-
|
|
1058
|
+
? 'text-warning'
|
|
1002
1059
|
: tone === 'err'
|
|
1003
1060
|
? 'text-red-600 dark:text-red-400'
|
|
1004
1061
|
: 'text-muted-foreground'
|