feat(mobile): swap admin page headers to PageHeader

Mechanical sweep replacing the plain h1+p header markup with the
mobile-aware PageHeader primitive across 12 admin pages: index,
backup, branding, documenso, email, import, invitations, monitoring,
onboarding, reminders, reports, webhooks. Webhooks "Add Webhook"
button preserved via the actions slot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-05-01 12:57:52 +02:00
parent c405124bc3
commit 71da6e8fdc
12 changed files with 101 additions and 107 deletions

View File

@@ -1,10 +1,9 @@
import { PageHeader } from '@/components/shared/page-header';
export default function BackupManagementPage() { export default function BackupManagementPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <PageHeader title="Backup Management" description="Manage system backups and restoration" />
<h1 className="text-2xl font-bold text-foreground">Backup Management</h1>
<p className="text-muted-foreground">Manage system backups and restoration</p>
</div>
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12"> <div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12">
<p className="text-lg font-medium text-muted-foreground">Coming in Layer 4</p> <p className="text-lg font-medium text-muted-foreground">Coming in Layer 4</p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">

View File

@@ -2,6 +2,7 @@ import {
SettingsFormCard, SettingsFormCard,
type SettingFieldDef, type SettingFieldDef,
} from '@/components/admin/shared/settings-form-card'; } from '@/components/admin/shared/settings-form-card';
import { PageHeader } from '@/components/shared/page-header';
const FIELDS: SettingFieldDef[] = [ const FIELDS: SettingFieldDef[] = [
{ {
@@ -47,13 +48,10 @@ const FIELDS: SettingFieldDef[] = [
export default function BrandingSettingsPage() { export default function BrandingSettingsPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <PageHeader
<h1 className="text-2xl font-semibold">Branding</h1> title="Branding"
<p className="text-sm text-muted-foreground"> description="Logo, primary color, app name, and email header/footer HTML used by the branded auth shell and outgoing email templates."
Logo, primary color, app name, and email header/footer HTML used by the branded auth shell />
and outgoing email templates.
</p>
</div>
<SettingsFormCard <SettingsFormCard
title="Identity" title="Identity"
description="App name, logo, and primary color." description="App name, logo, and primary color."

View File

@@ -3,6 +3,7 @@ import {
type SettingFieldDef, type SettingFieldDef,
} from '@/components/admin/shared/settings-form-card'; } from '@/components/admin/shared/settings-form-card';
import { DocumensoTestButton } from '@/components/admin/documenso/documenso-test-button'; import { DocumensoTestButton } from '@/components/admin/documenso/documenso-test-button';
import { PageHeader } from '@/components/shared/page-header';
const API_FIELDS: SettingFieldDef[] = [ const API_FIELDS: SettingFieldDef[] = [
{ {
@@ -48,13 +49,10 @@ const EOI_FIELDS: SettingFieldDef[] = [
export default function DocumensoSettingsPage() { export default function DocumensoSettingsPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <PageHeader
<h1 className="text-2xl font-semibold">Documenso & EOI</h1> title="Documenso & EOI"
<p className="text-sm text-muted-foreground"> description="API credentials and default EOI generation pathway. Use the test-connection button to verify a saved configuration before relying on it."
API credentials and default EOI generation pathway. Use the test-connection button to />
verify a saved configuration before relying on it.
</p>
</div>
<SettingsFormCard <SettingsFormCard
title="Documenso API" title="Documenso API"

View File

@@ -2,6 +2,7 @@ import {
SettingsFormCard, SettingsFormCard,
type SettingFieldDef, type SettingFieldDef,
} from '@/components/admin/shared/settings-form-card'; } from '@/components/admin/shared/settings-form-card';
import { PageHeader } from '@/components/shared/page-header';
const FIELDS: SettingFieldDef[] = [ const FIELDS: SettingFieldDef[] = [
{ {
@@ -79,13 +80,10 @@ const FIELDS: SettingFieldDef[] = [
export default function EmailSettingsPage() { export default function EmailSettingsPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <PageHeader
<h1 className="text-2xl font-semibold">Email Settings</h1> title="Email Settings"
<p className="text-sm text-muted-foreground"> description="Per-port outgoing email configuration. SMTP credentials and the From address default to environment variables when these fields are blank."
Per-port outgoing email configuration. SMTP credentials and the From address default to />
environment variables when these fields are blank.
</p>
</div>
<SettingsFormCard <SettingsFormCard
title="From address & signature" title="From address & signature"
description="Identity headers and shared HTML used by system-generated emails." description="Identity headers and shared HTML used by system-generated emails."

View File

@@ -1,10 +1,9 @@
import { PageHeader } from '@/components/shared/page-header';
export default function DataImportPage() { export default function DataImportPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <PageHeader title="Data Import" description="Import data from external sources" />
<h1 className="text-2xl font-bold text-foreground">Data Import</h1>
<p className="text-muted-foreground">Import data from external sources</p>
</div>
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12"> <div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12">
<p className="text-lg font-medium text-muted-foreground">Coming in Layer 4</p> <p className="text-lg font-medium text-muted-foreground">Coming in Layer 4</p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">

View File

@@ -1,15 +1,13 @@
import { InvitationsManager } from '@/components/admin/invitations/invitations-manager'; import { InvitationsManager } from '@/components/admin/invitations/invitations-manager';
import { PageHeader } from '@/components/shared/page-header';
export default function InvitationsPage() { export default function InvitationsPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <PageHeader
<h1 className="text-2xl font-semibold">Invitations</h1> title="Invitations"
<p className="text-sm text-muted-foreground"> description="Send a single-use invitation to a new CRM user. The recipient sets their own password via the link in the email."
Send a single-use invitation to a new CRM user. The recipient sets their own password via />
the link in the email.
</p>
</div>
<InvitationsManager /> <InvitationsManager />
</div> </div>
); );

View File

@@ -1,10 +1,9 @@
import { PageHeader } from '@/components/shared/page-header';
export default function OnboardingPage() { export default function OnboardingPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <PageHeader title="Onboarding" description="Guided setup for new port configurations" />
<h1 className="text-2xl font-bold text-foreground">Onboarding</h1>
<p className="text-muted-foreground">Guided setup for new port configurations</p>
</div>
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12"> <div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12">
<p className="text-lg font-medium text-muted-foreground">Coming in Layer 4</p> <p className="text-lg font-medium text-muted-foreground">Coming in Layer 4</p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">

View File

@@ -20,6 +20,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { PageHeader } from '@/components/shared/page-header';
interface AdminSection { interface AdminSection {
href: string; href: string;
@@ -165,13 +166,10 @@ export default async function AdminLandingPage({
const { portSlug } = await params; const { portSlug } = await params;
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <PageHeader
<h1 className="text-2xl font-semibold">Administration</h1> title="Administration"
<p className="text-sm text-muted-foreground"> description="Per-port configuration and system administration. Each card below opens a dedicated settings page."
Per-port configuration and system administration. Each card below opens a dedicated />
settings page.
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{SECTIONS.map((s) => { {SECTIONS.map((s) => {
const Icon = s.icon; const Icon = s.icon;

View File

@@ -2,6 +2,7 @@ import {
SettingsFormCard, SettingsFormCard,
type SettingFieldDef, type SettingFieldDef,
} from '@/components/admin/shared/settings-form-card'; } from '@/components/admin/shared/settings-form-card';
import { PageHeader } from '@/components/shared/page-header';
const DEFAULT_FIELDS: SettingFieldDef[] = [ const DEFAULT_FIELDS: SettingFieldDef[] = [
{ {
@@ -53,14 +54,10 @@ const DIGEST_FIELDS: SettingFieldDef[] = [
export default function ReminderSettingsPage() { export default function ReminderSettingsPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <PageHeader
<h1 className="text-2xl font-semibold">Reminders</h1> title="Reminders"
<p className="text-sm text-muted-foreground"> description="Default reminder behaviour for new interests and the optional daily-digest delivery window. Individual users can still configure their own digest preferences in Notifications → Preferences."
Default reminder behaviour for new interests and the optional daily-digest delivery />
window. Individual users can still configure their own digest preferences in Notifications
Preferences.
</p>
</div>
<SettingsFormCard <SettingsFormCard
title="Defaults for new interests" title="Defaults for new interests"

View File

@@ -1,10 +1,12 @@
import { PageHeader } from '@/components/shared/page-header';
export default function ScheduledReportsPage() { export default function ScheduledReportsPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <PageHeader
<h1 className="text-2xl font-bold text-foreground">Scheduled Reports</h1> title="Scheduled Reports"
<p className="text-muted-foreground">Configure and manage automated report delivery</p> description="Configure and manage automated report delivery"
</div> />
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12"> <div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12">
<p className="text-lg font-medium text-muted-foreground">Coming in Layer 3</p> <p className="text-lg font-medium text-muted-foreground">Coming in Layer 3</p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">

View File

@@ -2,6 +2,7 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { PageHeader } from '@/components/shared/page-header';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { import {
AlertDialog, AlertDialog,
@@ -36,7 +37,11 @@ export default function WebhooksPage() {
const [deleteTarget, setDeleteTarget] = useState<Webhook | null>(null); const [deleteTarget, setDeleteTarget] = useState<Webhook | null>(null);
const [expandedId, setExpandedId] = useState<string | null>(null); const [expandedId, setExpandedId] = useState<string | null>(null);
const [regenerating, setRegenerating] = useState<string | null>(null); const [regenerating, setRegenerating] = useState<string | null>(null);
const [newSecret, setNewSecret] = useState<{ webhookId: string; secret: string; masked: string } | null>(null); const [newSecret, setNewSecret] = useState<{
webhookId: string;
secret: string;
masked: string;
} | null>(null);
const loadWebhooks = useCallback(async () => { const loadWebhooks = useCallback(async () => {
try { try {
@@ -98,15 +103,20 @@ export default function WebhooksPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <PageHeader
<div> title="Webhooks"
<h1 className="text-2xl font-bold text-foreground">Webhooks</h1> description="Configure outgoing webhook integrations"
<p className="text-muted-foreground">Configure outgoing webhook integrations</p> actions={
</div> <Button
<Button onClick={() => { setEditTarget(null); setFormOpen(true); }}> onClick={() => {
Add Webhook setEditTarget(null);
</Button> setFormOpen(true);
</div> }}
>
Add Webhook
</Button>
}
/>
{loading ? ( {loading ? (
<p className="text-sm text-muted-foreground">Loading...</p> <p className="text-sm text-muted-foreground">Loading...</p>
@@ -116,7 +126,13 @@ export default function WebhooksPage() {
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-1">
Add a webhook to receive real-time notifications of CRM events. Add a webhook to receive real-time notifications of CRM events.
</p> </p>
<Button className="mt-4" onClick={() => { setEditTarget(null); setFormOpen(true); }}> <Button
className="mt-4"
onClick={() => {
setEditTarget(null);
setFormOpen(true);
}}
>
Add Webhook Add Webhook
</Button> </Button>
</div> </div>
@@ -141,17 +157,16 @@ export default function WebhooksPage() {
</div> </div>
<div className="flex items-center gap-2 shrink-0"> <div className="flex items-center gap-2 shrink-0">
<Button <Button variant="ghost" size="sm" onClick={() => handleToggleActive(webhook)}>
variant="ghost"
size="sm"
onClick={() => handleToggleActive(webhook)}
>
{webhook.isActive ? 'Disable' : 'Enable'} {webhook.isActive ? 'Disable' : 'Enable'}
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => { setEditTarget(webhook); setFormOpen(true); }} onClick={() => {
setEditTarget(webhook);
setFormOpen(true);
}}
> >
Edit Edit
</Button> </Button>
@@ -163,11 +178,7 @@ export default function WebhooksPage() {
> >
Delete Delete
</Button> </Button>
<Button <Button variant="ghost" size="sm" onClick={() => toggleExpand(webhook.id)}>
variant="ghost"
size="sm"
onClick={() => toggleExpand(webhook.id)}
>
{expandedId === webhook.id ? 'Collapse' : 'Details'} {expandedId === webhook.id ? 'Collapse' : 'Details'}
</Button> </Button>
</div> </div>
@@ -228,18 +239,26 @@ export default function WebhooksPage() {
onSuccess={loadWebhooks} onSuccess={loadWebhooks}
/> />
<AlertDialog open={!!deleteTarget} onOpenChange={(open) => { if (!open) setDeleteTarget(null); }}> <AlertDialog
open={!!deleteTarget}
onOpenChange={(open) => {
if (!open) setDeleteTarget(null);
}}
>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Delete Webhook</AlertDialogTitle> <AlertDialogTitle>Delete Webhook</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
Delete &quot;{deleteTarget?.name}&quot;? This will also delete all delivery history. This action Delete &quot;{deleteTarget?.name}&quot;? This will also delete all delivery history.
cannot be undone. This action cannot be undone.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDelete} className="bg-destructive text-destructive-foreground"> <AlertDialogAction
onClick={handleDelete}
className="bg-destructive text-destructive-foreground"
>
Delete Delete
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>

View File

@@ -7,6 +7,7 @@ import { apiFetch } from '@/lib/api/client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ServiceHealthCard } from './service-health-card'; import { ServiceHealthCard } from './service-health-card';
import { QueueOverview } from './queue-overview'; import { QueueOverview } from './queue-overview';
import { PageHeader } from '@/components/shared/page-header';
import type { import type {
HealthStatus, HealthStatus,
QueueStatus, QueueStatus,
@@ -17,16 +18,14 @@ import type {
export function SystemMonitoringDashboard() { export function SystemMonitoringDashboard() {
const { data: healthData } = useQuery({ const { data: healthData } = useQuery({
queryKey: ['system', 'health'], queryKey: ['system', 'health'],
queryFn: () => queryFn: () => apiFetch<{ data: HealthStatus }>('/api/v1/admin/health').then((r) => r.data),
apiFetch<{ data: HealthStatus }>('/api/v1/admin/health').then((r) => r.data),
staleTime: 30_000, staleTime: 30_000,
refetchInterval: 30_000, refetchInterval: 30_000,
}); });
const { data: queuesData } = useQuery({ const { data: queuesData } = useQuery({
queryKey: ['system', 'queues'], queryKey: ['system', 'queues'],
queryFn: () => queryFn: () => apiFetch<{ data: QueueStatus[] }>('/api/v1/admin/queues').then((r) => r.data),
apiFetch<{ data: QueueStatus[] }>('/api/v1/admin/queues').then((r) => r.data),
staleTime: 10_000, staleTime: 10_000,
refetchInterval: 10_000, refetchInterval: 10_000,
}); });
@@ -47,11 +46,10 @@ export function SystemMonitoringDashboard() {
return ( return (
<div className="space-y-8"> <div className="space-y-8">
{/* Page header */} <PageHeader
<div> title="System Monitoring"
<h1 className="text-2xl font-bold text-foreground">System Monitoring</h1> description="Real-time health, queue status and connection tracking"
<p className="text-muted-foreground">Real-time health, queue status and connection tracking</p> />
</div>
{/* Service health */} {/* Service health */}
<section className="space-y-3"> <section className="space-y-3">
@@ -79,10 +77,7 @@ export function SystemMonitoringDashboard() {
) : ( ) : (
<div className="flex gap-3"> <div className="flex gap-3">
{[1, 2, 3, 4].map((i) => ( {[1, 2, 3, 4].map((i) => (
<div <div key={i} className="h-[88px] w-[160px] rounded-xl border bg-card animate-pulse" />
key={i}
className="h-[88px] w-[160px] rounded-xl border bg-card animate-pulse"
/>
))} ))}
</div> </div>
)} )}
@@ -124,9 +119,7 @@ export function SystemMonitoringDashboard() {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-3xl font-bold"> <p className="text-3xl font-bold">{queues.reduce((sum, q) => sum + q.active, 0)}</p>
{queues.reduce((sum, q) => sum + q.active, 0)}
</p>
</CardContent> </CardContent>
</Card> </Card>
</section> </section>
@@ -141,10 +134,7 @@ export function SystemMonitoringDashboard() {
) : ( ) : (
<div className="grid grid-cols-5 gap-3"> <div className="grid grid-cols-5 gap-3">
{[1, 2, 3, 4, 5].map((i) => ( {[1, 2, 3, 4, 5].map((i) => (
<div <div key={i} className="h-[110px] rounded-xl border bg-card animate-pulse" />
key={i}
className="h-[110px] rounded-xl border bg-card animate-pulse"
/>
))} ))}
</div> </div>
)} )}
@@ -159,8 +149,7 @@ export function SystemMonitoringDashboard() {
function RecentErrorsPanel() { function RecentErrorsPanel() {
const { data: errorsData } = useQuery({ const { data: errorsData } = useQuery({
queryKey: ['system', 'errors'], queryKey: ['system', 'errors'],
queryFn: () => queryFn: () => apiFetch<{ data: RecentError[] }>('/api/v1/admin/errors').then((r) => r.data),
apiFetch<{ data: RecentError[] }>('/api/v1/admin/errors').then((r) => r.data),
staleTime: 30_000, staleTime: 30_000,
refetchInterval: 30_000, refetchInterval: 30_000,
}); });