import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { apiFetch } from '../lib/api'; import PageScanner from './PageScanner'; import ScanSlideIn from './ScanSlideIn'; import { DashboardSkeleton } from './Skeleton'; import type { PageScanSummary } from '../lib/types'; export default function Dashboard() { const [selectedScan, setSelectedScan] = useState(null); const { data: pageScans = {}, isLoading, isError } = useQuery({ queryKey: ['page-scans'], queryFn: () => apiFetch>('/page-scans'), staleTime: 0, }); if (isLoading) return ; if (isError) return
Failed to load scan data. Please refresh the page.
; // Aggregate issue counts across all scanned pages, including best-practice // items — they still carry a real severity and are counted in scan.failed, // so excluding them here would under-count Warnings/Notices vs the totals // shown elsewhere (e.g. the "Issues by Page" bar and ScanSlideIn's modal). const allIssues = Object.values(pageScans).flatMap((s) => s.issues ?? []); const critical = allIssues.filter((i) => i.severity === 'critical').length; const serious = allIssues.filter((i) => i.severity === 'serious').length; const warning = allIssues.filter((i) => i.severity === 'warning').length; const notice = allIssues.filter((i) => i.severity === 'notice').length; // "Issues found" is the full total — matches the "All" tab in the frontend // overlay/ScanSlideIn (critical + serious + warning + notice, WCAG + best-practice). const total = allIssues.length; const passed = Object.values(pageScans).reduce((sum, s) => sum + (s.passed ?? 0), 0); const hasData = Object.keys(pageScans).length > 0; return (
{/* Stats row */} {hasData && ( <>

Automated accessibility score based on detected issues.{' '} Some issues may require manual testing. ⓘ

💡 Tip: Fix critical issues first for the biggest accessibility impact.

)} {/* Issues by page chart */} {hasData && (

Issues by Page

Top pages ranked by number of accessibility issues

)} {/* Pages & Posts scanner (main dashboard view) */} setSelectedScan(null)} />
); } // ── Issues by page bar chart ─────────────────────────────────── function IssuesByPage({ pageScans, onOpenScan }: { pageScans: Record; onOpenScan: (scan: PageScanSummary) => void }) { // Ranked and counted by the full issues array (matches the frontend "All" tab), // not the separately-stored scan.failed field, so this can't drift out of sync. const pages = Object.values(pageScans) .filter((s) => (s.issues ?? []).length > 0) .sort((a, b) => (b.issues ?? []).length - (a.issues ?? []).length) .slice(0, 7); if (pages.length === 0) return (

No issues detected across scanned pages.

); const maxTotal = pages[0].issues?.length || 1; return (
{pages.map((scan) => { const scanIssues = scan.issues ?? []; const crit = scanIssues.filter((i) => i.severity === 'critical').length; const ser = scanIssues.filter((i) => i.severity === 'serious').length; const warn = scanIssues.filter((i) => i.severity === 'warning').length; const note = scanIssues.filter((i) => i.severity === 'notice').length; const total = scanIssues.length; const pathname = (() => { try { const u = new URL(scan.url); // Strip a trailing slash that trailingslashit() incorrectly appended after query params // (e.g. http://example.com/?cat=1/ → /?cat=1, not /?cat=1/) const search = u.search.replace(/\/$/, ''); return (u.pathname + search) || '/'; } catch { return scan.url; } })(); const label = scan.title || pathname; const barPct = (total / maxTotal) * 100; return (
{crit > 0 && } {ser > 0 && } {warn > 0 && } {note > 0 && }
{total}
); })}
Critical Serious Warning Notice
); } // ── Stat card ────────────────────────────────────────────────── function StatCard({ label, value, variant }: { label: string; value: number; variant: string }) { return (
{value} {label}
); }