import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useReport } from '../hooks/useReport'; import { useAI } from '../hooks/useAI'; import { apiFetch } from '../lib/api'; import { markdownToHtml } from '../lib/markdown'; import ScanSlideIn from './ScanSlideIn'; import { ReportSkeleton } from './Skeleton'; import type { PageScanSummary } from '../lib/types'; // ── Helpers ─────────────────────────────────────────────────── function scoreColor(s: number) { return s >= 90 ? '#22c55e' : s >= 75 ? '#84cc16' : s >= 55 ? '#f59e0b' : s >= 35 ? '#f97316' : '#ef4444'; } function exportCsv(pageScans: Record) { const cols = ['Page URL', 'WCAG', 'Severity', 'Issue Title', 'Description', 'Selector', 'Elements']; const esc = (v: string) => `"${String(v ?? '').replace(/"/g, '""')}"`; const lines = [cols.map(esc).join(',')]; Object.values(pageScans).forEach((scan) => { (scan.issues ?? []).forEach((iss) => { lines.push([ scan.url, iss.wcag ?? '', iss.severity, iss.title, iss.description, iss.selector ?? '', iss.count, ].map(String).map(esc).join(',')); }); }); const blob = new Blob([lines.join('\n')], { type: 'text/csv' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `accessibility-audit-${new Date().toISOString().slice(0, 10)}.csv`; a.click(); URL.revokeObjectURL(a.href); } // ── Summary bar ─────────────────────────────────────────────── function SummaryBar({ pageScans }: { pageScans: Record }) { const all = Object.values(pageScans).flatMap((s) => s.issues ?? []); const critical = all.filter((i) => i.severity === 'critical').length; const warning = all.filter((i) => i.severity === 'warning').length; const notice = all.filter((i) => i.severity === 'notice').length; const pages = Object.keys(pageScans).length; return (
{all.length} Issues found
{critical} Critical issues
{warning} Needs attention
{notice} Notices
{pages} Pages Scanned
); } // ── Main report view ────────────────────────────────────────── export default function ReportView() { const { data: report } = useReport(); const { run, hasProvider } = useAI(); const [summary, setSummary] = useState(null); const [aiError, setAiError] = useState(null); const [searchInput, setSearchInput] = useState(''); const [search, setSearch] = useState(''); const [selectedScan, setSelectedScan] = useState(null); const { data: pageScans = {}, isLoading: scansLoading } = useQuery({ queryKey: ['page-scans', search], queryFn: () => apiFetch>(search ? `/page-scans?search=${encodeURIComponent(search)}` : '/page-scans'), }); if (scansLoading) return ; if (Object.keys(pageScans).length === 0 && !search) { return (

No issues detected

This scan didn't detect any accessibility issues. Some issues may require manual testing with assistive technologies.

); } const allScans = Object.values(pageScans).sort((a, b) => b.failed - a.failed); const displayScans = allScans; const generateSummary = async () => { setSummary(null); setAiError(null); const allIssues = allScans.flatMap((s) => s.issues ?? []); const critical = allIssues.filter((i) => i.severity === 'critical').length; const warnings = allIssues.filter((i) => i.severity === 'warning').length; try { const result = await run.mutateAsync({ task: 'report', params: { score: String(report?.score ?? 0), critical: String(critical), warnings: String(warnings) }, }); if (result.error) setAiError(result.error); else setSummary(result.text ?? ''); } catch (e) { setAiError((e as Error).message); } }; return (
{/* Page issues slide-in */} setSelectedScan(null)} /> {/* ── Header ── */}

Accessibility Audit Report

{report?.scanned_at && ( {new Date(report.scanned_at).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })} )}
{/* ── Automated scan disclaimer ── */}
This report highlights issues detected through automated scanning. Some accessibility issues may require manual testing for full WCAG 2.1 AA compliance.
{/* ── WCAG Standard notice ── */}
Standard: WCAG 2.1 Level AA Engine: axe-core 4.x Scope: Browser-based automated scan
{/* ── Summary bar ── */} {/* ── AI summary ── */} {summary && (
AI Compliance Summary
)} {aiError &&

{aiError}

} {/* ── Toolbar ── */}
setSearchInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { setSearch(searchInput.trim()); } }} aria-label="Search pages" /> {searchInput && ( )}
{/* ── Pages table ── */} {displayScans.length === 0 ? (

No pages match your search.

) : (
{displayScans.map((scan, i) => { const c = scoreColor(scan.score); return ( ); })}
# Page URL Grade Score Issues Last Scanned
{i + 1} {(() => { try { return new URL(scan.url).pathname || '/'; } catch { return scan.url; } })()} {scan.url} {scan.grade} {scan.score} 0 ? '#ef4444' : '#22c55e', fontWeight: 600 }}> {scan.failed} {new Date(scan.scanned_at).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', })}
)}

AccessMate uses automated scanning to identify and help fix accessibility issues. Automated tools can detect only a subset of accessibility problems. For full WCAG 2.1 AA compliance, manual testing — including testing with assistive technologies such as screen readers — is recommended.

AccessMate helps you find and fix accessibility issues faster. It does not replace a full manual accessibility audit.

); }