import { useEffect, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { apiFetch } from '../lib/api'; import type { StatementData } from '../lib/types'; // ── Constants ───────────────────────────────────────────────── const STEPS = ['Basic Information', 'Your Efforts', 'Conformance Status', 'Technical Specs', 'Feedback']; const PREDEFINED_EFFORTS = [ 'Accessibility is part of our mission statement.', 'Accessibility is part of our internal policies.', 'Accessibility is integrated into our procurement practices.', 'An accessibility officer or official has been appointed.', 'Continual accessibility training is provided for employees.', 'Clear accessibility targets and responsibilities exist within the organisation.', 'Formal accessibility quality assurance methods exist within the organisation.', ]; const BROWSERS = ['Chrome', 'Firefox', 'Safari', 'Edge', 'Internet Explorer']; const OS_LIST = ['Windows', 'macOS', 'iOS', 'Android', 'Linux']; const AT_LIST = ['JAWS', 'NVDA', 'VoiceOver', 'TalkBack', 'Dragon NaturallySpeaking']; const TECH_LIST = ['HTML', 'CSS', 'JavaScript', 'WAI-ARIA']; const WCAG_STANDARDS = [ { value: 'wcag22-aa', label: 'WCAG 2.2 Level AA' }, { value: 'wcag21-aaa', label: 'WCAG 2.1 Level AAA' }, { value: 'wcag21-aa', label: 'WCAG 2.1 Level AA' }, { value: 'wcag21-a', label: 'WCAG 2.1 Level A' }, { value: 'wcag20-aaa', label: 'WCAG 2.0 Level AAA' }, { value: 'wcag20-aa', label: 'WCAG 2.0 Level AA' }, { value: 'wcag20-a', label: 'WCAG 2.0 Level A' }, { value: 'section508', label: 'Section 508' }, { value: 'other', label: 'Other' }, ]; const CONFORMANCE_LEVELS = [ { value: 'fully-conformant', label: 'Fully conformant', desc: 'The content fully conforms to the accessibility standard without any exceptions.' }, { value: 'partially-conformant', label: 'Partially conformant', desc: 'Some parts of the content do not fully conform to the accessibility standard.' }, { value: 'non-conformant', label: 'Non conformant', desc: 'The content does not conform to the accessibility standard.' }, { value: 'not-assessed', label: 'Not assessed', desc: 'The content has not been evaluated or the evaluation results are not available.' }, ]; const EMPTY: StatementData = { status: 'none', page_id: 0, page_url: '', edit_url: '', updated_at: '', basic_info: { org_name: '', website: '', website_name: '' }, efforts: { selected: [], additional: [] }, conformance:{ standard: 'wcag21-aa', custom_standard: '', level: 'partially-conformant', additional_notes: '' }, tech_specs: { browsers: ['Chrome', 'Firefox', 'Safari', 'Edge'], operating_systems: ['Windows', 'macOS'], assistive_tech: [], technology_reliance: ['HTML', 'CSS', 'JavaScript'] }, feedback: { phone: '', email: '', address: '', response_duration: '2 business days' }, }; // ── Stepper ─────────────────────────────────────────────────── function Stepper({ step, total, labels }: { step: number; total: number; labels: string[] }) { return (
{Array.from({ length: total }, (_, i) => (
{i < step ? '✓' : i + 1}
{i === step && {labels[i]}} {i < total - 1 &&
}
))}
); } // ── CheckGroup ──────────────────────────────────────────────── function CheckGroup({ options, selected, onChange, }: { options: string[]; selected: string[]; onChange: (v: string[]) => void }) { const toggle = (v: string) => onChange(selected.includes(v) ? selected.filter((x) => x !== v) : [...selected, v]); return (
{options.map((o) => ( ))}
); } // ── Main component ──────────────────────────────────────────── export default function StatementGenerator() { const qc = useQueryClient(); const { data: saved } = useQuery({ queryKey: ['statement'], queryFn: () => apiFetch('/statement'), }); const [data, setData] = useState(EMPTY); const [step, setStep] = useState(0); const [addEffort, setAddEffort] = useState(''); const [showAdd, setShowAdd] = useState(false); const [editing, setEditing] = useState(false); const [htmlStripped, setHtmlStripped] = useState(false); const [saveOk, setSaveOk] = useState(false); useEffect(() => { if (saved) { setData(saved); if (saved.status === 'none') setEditing(true); } }, [saved]); const save = useMutation({ mutationFn: (publish: boolean) => apiFetch('/statement', { method: 'POST', body: JSON.stringify({ ...data, publish }), }), onSuccess: (result) => { // The server strips HTML tags from every text field for security. Detect // that here (rather than block it) so the user isn't left wondering why // their input silently disappeared. const submittedTextFields = [ data.basic_info.org_name, data.basic_info.website_name, data.conformance.custom_standard, data.conformance.additional_notes, data.feedback.phone, data.feedback.address, data.feedback.response_duration, ]; setHtmlStripped(submittedTextFields.some((v) => /[<>]/.test(v))); setData(result); setEditing(false); qc.invalidateQueries({ queryKey: ['statement'] }); setSaveOk(true); setTimeout(() => setSaveOk(false), 3000); }, }); // ── helpers ── const setBasic = (k: keyof StatementData['basic_info'], v: string) => setData((d) => ({ ...d, basic_info: { ...d.basic_info, [k]: v } })); const setConformance = (k: keyof StatementData['conformance'], v: string) => setData((d) => ({ ...d, conformance: { ...d.conformance, [k]: v } })); const setFeedback = (k: keyof StatementData['feedback'], v: string) => setData((d) => ({ ...d, feedback: { ...d.feedback, [k]: v } })); const setTechSpecs = (k: keyof StatementData['tech_specs'], v: string[]) => setData((d) => ({ ...d, tech_specs: { ...d.tech_specs, [k]: v } })); const setEfforts = (k: keyof StatementData['efforts'], v: string[]) => setData((d) => ({ ...d, efforts: { ...d.efforts, [k]: v } })); const isValidEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim()); const isValidUrl = (url: string) => { try { return ['http:', 'https:'].includes(new URL(url.trim()).protocol); } catch { return false; } }; const canNext = () => { if (step === 0) return !!(data.basic_info.org_name.trim() && isValidUrl(data.basic_info.website) && data.basic_info.website_name.trim()); return true; }; // ── Success view ── if (!editing && data.status !== 'none') { return (

Accessibility Statement

Generate and manage your website's accessibility statement.

{htmlStripped && (

⚠ HTML tags aren't supported in statement fields and were removed from your input for security. Please review the fields below.

)}
Statement generated{saveOk && Saved.}
{data.updated_at && (
Last updated: {data.updated_at}
)}
{data.page_url && ( 👁 Preview )} {data.edit_url && ( ✏ Edit in WP )}
); } // ── Wizard ── return (

Accessibility Statement

Generate and manage your website's accessibility statement.

Generate statement

{/* ── Step 1: Basic Information ── */} {step === 0 && (

{STEPS[0]}

To get started, tell us about your business and website. Fields marked with * must be filled in.

Example: Example Inc.

setBasic('org_name', e.target.value)} placeholder="Your organisation name" />

Example: https://yourwebsite.com

setBasic('website', e.target.value)} placeholder="https://yourwebsite.com" /> {!isValidUrl(data.basic_info.website) && (

Enter a valid website URL, including https:// or http://.

)}

Example: Example Site

setBasic('website_name', e.target.value)} placeholder="Your website name" />
)} {/* ── Step 2: Your Efforts ── */} {step === 1 && (

{STEPS[1]}

Select any current actions your organisation is taking to ensure web accessibility.

setEfforts('selected', v)} /> {data.efforts.additional.map((eff, i) => (
{ const next = [...data.efforts.additional]; next[i] = e.target.value; setEfforts('additional', next); }} />
))} {showAdd ? (
setAddEffort(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && addEffort.trim()) { setEfforts('additional', [...data.efforts.additional, addEffort.trim()]); setAddEffort(''); setShowAdd(false); } }} placeholder="Describe your effort…" />
) : ( )}
)} {/* ── Step 3: Conformance Status ── */} {step === 2 && (

{STEPS[2]}

Which accessibility standard have you been following? *
{WCAG_STANDARDS.map((s) => ( ))}
{data.conformance.standard === 'other' && ( <> setConformance('custom_standard', e.target.value)} placeholder="Describe the standard" /> )}
Current content conformance status? *
{CONFORMANCE_LEVELS.map((l) => ( ))}

Example: While aiming for WCAG 2.1 Level AA conformance, we also implemented some Level AAA criteria.