'use client'; /** * /admin: the first screen after signing in (docs/admin-ux-concept.md). * * Says what needs attention before it is asked for (unread messages, doctor * findings), then the site at a glance. Everything on it links to the screen * where it is acted on. It only reads the existing admin endpoints; a failed * one leaves its part out rather than failing the page. */ import { useEffect, useState } from 'react'; import Link from 'next/link'; import * as Icons from './Icons'; import { adminGet } from './useAdminFetch'; import type { DoctorFinding, DoctorLevel } from '@/lib/admin/doctor'; import { DOCTOR_LEVEL_EVENT } from './systemHealth'; interface OverviewData { gallery: { hero?: unknown; subpages?: Array<{ enabled?: boolean }> } | null; journal: Array<{ frontmatter: { draft?: boolean } }> | null; messages: Array<{ read: boolean; name: string }> | null; analytics: { days?: Record } | null; doctor: { level: DoctorLevel; findings: DoctorFinding[] } | null; } /** Where a doctor finding is fixed; mirrors FIXES in DiagnosticsView. */ const FIX_ROUTE: Record = { legal: '/admin/settings/legal', contact: '/admin/settings/legal', privacy: '/admin/settings/legal', passwords: '/admin/settings/security', 'album-ids': '/admin/pages', 'albums-shared': '/admin/pages', }; function viewsLastDays( days: Record | undefined, n: number, ): number { if (!days) return 0; const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - (n - 1)); const from = cutoff.toISOString().slice(0, 10); return Object.entries(days) .filter(([date]) => date >= from) .reduce((sum, [, d]) => sum + (d.pageviews || 0), 0); } function greeting(): string { const h = new Date().getHours(); if (h < 5) return 'Good night'; if (h < 12) return 'Good morning'; if (h < 18) return 'Good afternoon'; return 'Good evening'; } export default function AdminOverview() { const [data, setData] = useState(null); useEffect(() => { let live = true; Promise.all([ adminGet<{ gallery: OverviewData['gallery'] }>('/api/admin/gallery'), adminGet<{ entries: NonNullable }>('/api/admin/journal'), adminGet<{ messages: NonNullable }>('/api/admin/messages'), adminGet>('/api/admin/analytics'), adminGet>('/api/admin/doctor'), ]).then(([gallery, journal, messages, analytics, doctor]) => { if (!live) return; setData({ gallery: gallery.data?.gallery ?? null, journal: journal.data?.entries ?? null, messages: messages.data?.messages ?? null, analytics: analytics.data ?? null, doctor: doctor.data ?? null, }); // The sidebar badge takes the level from here instead of asking again. if (doctor.data?.level) { window.dispatchEvent(new CustomEvent(DOCTOR_LEVEL_EVENT, { detail: doctor.data.level })); } }); return () => { live = false; }; }, []); const subpages = data?.gallery?.subpages ?? []; const published = subpages.filter((sp) => sp.enabled !== false).length; const heroRaw = data?.gallery?.hero; const heroCount = Array.isArray(heroRaw) ? heroRaw.length : heroRaw ? 1 : 0; const entries = data?.journal ?? []; const drafts = entries.filter((e) => e.frontmatter.draft).length; const unread = (data?.messages ?? []).filter((m) => !m.read); const views7 = viewsLastDays(data?.analytics?.days, 7); const findings = (data?.doctor?.findings ?? []).filter((f) => f.level !== 'ok'); const attention: Array<{ tone: 'accent' | 'warn' | 'error'; title: string; detail?: string; href: string; }> = []; if (unread.length) { attention.push({ tone: 'accent', title: `${unread.length} unread message${unread.length === 1 ? '' : 's'}`, detail: `From ${[...new Set(unread.map((m) => m.name))].slice(0, 3).join(', ')}`, href: '/admin/messages', }); } for (const f of findings) { attention.push({ tone: f.level === 'error' ? 'error' : 'warn', title: f.title, detail: f.detail, href: FIX_ROUTE[f.id] ?? '/admin/diagnostics', }); } return (

Overview

{greeting()}.

{data ? attention.length ? `${attention.length} thing${attention.length === 1 ? '' : 's'} could use a look.` : 'Everything is in order.' : 'Checking the site…'}

View site Edit pages

Needs attention

{!data ? (
) : attention.length ? (
{attention.map((a, i) => (
) : (

Nothing waiting. New messages and problems show up here.

)}

At a glance

Pages {data ? published : '–'} {subpages.length - published > 0 ? `${subpages.length - published} switched off` : 'all published'} Hero photos {data ? heroCount : '–'} on the home page Journal {data ? entries.length : '–'} {drafts ? `${drafts} draft${drafts === 1 ? '' : 's'}` : 'no drafts'} Views · 7 days {data ? views7.toLocaleString() : '–'} cookieless count

Quick actions

New journal entry Messages Theme Diagnostics
); }