admin redesign: topbar state zone, podcast hub tabs, AnalyticsPanel CSS classes, useAutosave

This commit is contained in:
nmemmert
2026-06-01 11:45:52 -04:00
parent a58d75c425
commit b9ed8761db
5 changed files with 277 additions and 54 deletions
+51
View File
@@ -0,0 +1,51 @@
import type { ReactNode } from 'react'
interface AdminCollapsibleCardProps {
title: string
subtitle?: string
hint?: string
previewUrl?: string
children: ReactNode
open?: boolean
className?: string
}
/**
* Reusable collapsible card for admin forms.
* Replaces the repeated <details>/<summary> pattern throughout AdminPage.
*/
export function AdminCollapsibleCard({
title,
subtitle,
hint = 'Expand to edit',
previewUrl,
children,
open = false,
className = '',
}: AdminCollapsibleCardProps) {
return (
<details className={`admin-collapsible-card ${className}`.trim()} open={open}>
<summary className="admin-collapsible-summary">
<div>
<strong>{title}</strong>
{subtitle && <p>{subtitle}</p>}
</div>
<div className="admin-collapsible-actions">
{previewUrl && (
<a
href={previewUrl}
target="_blank"
rel="noopener noreferrer"
className="btn-admin-secondary"
onClick={e => e.stopPropagation()}
>
Preview
</a>
)}
<span className="admin-collapsible-hint">{hint}</span>
</div>
</summary>
<div className="admin-collapsible-body">{children}</div>
</details>
)
}
+29 -37
View File
@@ -238,21 +238,21 @@ export function AnalyticsPanel({
</div>
{/* Chart Tabs */}
<div className="admin-actions admin-actions--maintenance" style={{ marginBottom: '1.5rem' }}>
<button type="button" className={`btn-admin-reset${chartTab === 'overview' ? ' btn-admin-reset--active' : ''}`} onClick={() => setChartTab('overview')}>Overview</button>
<button type="button" className={`btn-admin-reset${chartTab === 'breakdown' ? ' btn-admin-reset--active' : ''}`} onClick={() => setChartTab('breakdown')}>Bot Breakdown</button>
<button type="button" className={`btn-admin-reset${chartTab === 'geographic' ? ' btn-admin-reset--active' : ''}`} onClick={() => setChartTab('geographic')}>Geographic</button>
<div className="admin-tabs admin-tabs--charts">
<button type="button" className={`admin-tab${chartTab === 'overview' ? ' admin-tab--active' : ''}`} onClick={() => setChartTab('overview')}>Overview</button>
<button type="button" className={`admin-tab${chartTab === 'breakdown' ? ' admin-tab--active' : ''}`} onClick={() => setChartTab('breakdown')}>Bot Breakdown</button>
<button type="button" className={`admin-tab${chartTab === 'geographic' ? ' admin-tab--active' : ''}`} onClick={() => setChartTab('geographic')}>Geographic</button>
</div>
{/* Overview Charts */}
{chartTab === 'overview' && (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))', gap: '2rem', marginBottom: '2rem' }}>
<div style={{ backgroundColor: '#1a1a15', padding: '1.5rem', borderRadius: '0.5rem', border: '1px solid #2a2518' }}>
<h3 style={{ marginBottom: '1rem' }}>7-Day Trend (Real vs Bot)</h3>
<div className="admin-analytics-grid">
<div className="admin-analytics-card">
<h3 className="admin-analytics-card-title">7-Day Trend (Real vs Bot)</h3>
<Line data={dailyChartData} options={chartOptions} />
</div>
<div style={{ backgroundColor: '#1a1a15', padding: '1.5rem', borderRadius: '0.5rem', border: '1px solid #2a2518' }}>
<h3 style={{ marginBottom: '1rem' }}>Traffic Composition</h3>
<div className="admin-analytics-card">
<h3 className="admin-analytics-card-title">Traffic Composition</h3>
<Doughnut data={trafficRatioData} options={{...chartOptions, plugins: {...chartOptions.plugins}}} />
</div>
</div>
@@ -260,25 +260,25 @@ export function AnalyticsPanel({
{/* Bot Breakdown Charts */}
{chartTab === 'breakdown' && (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))', gap: '2rem', marginBottom: '2rem' }}>
<div style={{ backgroundColor: '#1a1a15', padding: '1.5rem', borderRadius: '0.5rem', border: '1px solid #2a2518' }}>
<h3 style={{ marginBottom: '1rem' }}>Bot Sources</h3>
<div className="admin-analytics-grid">
<div className="admin-analytics-card">
<h3 className="admin-analytics-card-title">Bot Sources</h3>
{botReasons.length === 0 ? (
<p className="admin-stats-note">No bot traffic detected.</p>
) : (
<Pie data={botPieData} options={{...chartOptions, plugins: {...chartOptions.plugins}}} />
)}
</div>
<div style={{ backgroundColor: '#1a1a15', padding: '1.5rem', borderRadius: '0.5rem', border: '1px solid #2a2518' }}>
<h3 style={{ marginBottom: '1rem' }}>Bot Detection Details</h3>
<div className="admin-analytics-card">
<h3 className="admin-analytics-card-title">Bot Detection Details</h3>
{botReasons.length === 0 ? (
<p className="admin-stats-note">No bots detected yet.</p>
) : (
<ul style={{ listStyle: 'none', padding: 0 }}>
<ul className="admin-bot-reason-list">
{botReasons.map((reason: { reason: string; count: number }) => (
<li key={reason.reason} style={{ padding: '0.5rem 0', display: 'flex', justifyContent: 'space-between', borderBottom: '1px solid #2a2518' }}>
<li key={reason.reason} className="admin-bot-reason-item">
<span>{reason.reason.replace(/-/g, ' ')}</span>
<strong style={{ color: '#c9a84c' }}>{reason.count.toLocaleString()}</strong>
<strong>{reason.count.toLocaleString()}</strong>
</li>
))}
</ul>
@@ -289,9 +289,9 @@ export function AnalyticsPanel({
{/* Geographic Charts */}
{chartTab === 'geographic' && (
<div style={{ marginBottom: '2rem' }}>
<div style={{ backgroundColor: '#1a1a15', padding: '1.5rem', borderRadius: '0.5rem', border: '1px solid #2a2518', marginBottom: '2rem' }}>
<h3 style={{ marginBottom: '1rem' }}>Top Pages</h3>
<div className="admin-analytics-geo">
<div className="admin-analytics-card admin-analytics-card--wide">
<h3 className="admin-analytics-card-title">Top Pages</h3>
<Bar data={topPathsChartData} options={{...chartOptions, indexAxis: 'y' as const}} />
</div>
<GeoMaps
@@ -302,13 +302,13 @@ export function AnalyticsPanel({
)}
{/* Recent Visitors Table */}
<div className="admin-stats-head admin-stats-head--visitors" style={{ marginTop: '2rem' }}>
<div className="admin-stats-head admin-stats-head--visitors">
<h2>Visitor Details</h2>
<p>Real-time tracking of visitor activity, geography, and returning behavior.</p>
</div>
<p className="admin-privacy-note">Privacy: visitor analytics only run after cookie consent. IPs below are masked.</p>
<div className="admin-stats-grid" style={{ marginBottom: '1.5rem' }}>
<div className="admin-stats-grid admin-stats-grid--visitor">
<article><h3>Total Visits</h3><p>{stats.visitors.totalVisits.toLocaleString()}</p></article>
<article><h3>Unique Visitors</h3><p>{stats.visitors.uniqueVisitors.toLocaleString()}</p></article>
<article><h3>Returning Visits</h3><p>{stats.visitors.returningVisits.toLocaleString()}</p></article>
@@ -355,23 +355,15 @@ export function AnalyticsPanel({
{stats.visitors.recentVisits.map(row => (
<tr key={`${row.visitorId}-${row.at}`}>
<td>{formatDate(row.at)}</td>
<td><code style={{ fontSize: '0.8rem', color: '#aaa' }}>{maskIp(row.ip)}</code></td>
<td><code className="admin-visitor-ip">{maskIp(row.ip)}</code></td>
<td>{row.country || '—'}</td>
<td style={{ maxWidth: '220px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={row.path}>{row.path}</td>
<td className="admin-visitor-path" title={row.path}>{row.path}</td>
<td>
<span style={{
display: 'inline-block',
padding: '0.1rem 0.5rem',
borderRadius: '999px',
fontSize: '0.75rem',
backgroundColor: row.returningVisitor ? '#1a3a1a' : '#1a1a15',
color: row.returningVisitor ? '#6dbf6d' : '#888',
border: `1px solid ${row.returningVisitor ? '#2a5a2a' : '#333'}`,
}}>
<span className={`admin-visitor-tag admin-visitor-tag--${row.returningVisitor ? 'returning' : 'new'}`}>
{row.returningVisitor ? 'Returning' : 'New'}
</span>
</td>
<td style={{ textAlign: 'center', color: '#c9a84c' }}>{row.visitCount}</td>
<td className="admin-visitor-count">{row.visitCount}</td>
</tr>
))}
</tbody>
@@ -381,7 +373,7 @@ export function AnalyticsPanel({
</div>
{/* Contact Summary */}
<div className="admin-stats-head admin-stats-head--visitors" style={{ marginTop: '2rem' }}>
<div className="admin-stats-head admin-stats-head--visitors">
<h2>Contact Summary</h2>
<p>Submission totals from the contact form.</p>
</div>
@@ -391,7 +383,7 @@ export function AnalyticsPanel({
</div>
{/* Deployment & Cache Status */}
<div className="admin-stats-head admin-stats-head--visitors" style={{ marginTop: '2rem' }}>
<div className="admin-stats-head admin-stats-head--visitors">
<h2>Deployment &amp; Cache Status</h2>
<p>Current deployment metadata and cache purge health for the live app.</p>
</div>
@@ -403,7 +395,7 @@ export function AnalyticsPanel({
</div>
{/* Data Management */}
<div className="admin-stats-head admin-stats-head--visitors" style={{ marginTop: '2rem' }}>
<div className="admin-stats-head admin-stats-head--visitors">
<h2>Data Management</h2>
<p>Export, backup, or retain only recent analytics data.</p>
</div>