Standalone example · v1
Form errors you can act on.
This is the shop contact form, including its actual validation and simulated response controls.
Prepare empties the contact draft and result. There is no login, email delivery or support integration.
View the shared source
These are the actual source files used by this fixture. A generated snapshot is checked against the implementation before each build validation. Imports and reusable helpers remain visible; this is not a separate teaching copy.
src/components/features/contact/contact-form.tsx
'use client';
import {useEffect,useRef,useState} from 'react';
import {useTranslations} from 'next-intl';
import {Button} from '@/components/ui/button';
import {FormErrors,FormField} from '@/components/shared/forms/form-field';
import {useDemoState} from '@/lib/state/use-demo-state';
import {readState} from '@/lib/state/registry';
import {beginContact,contactErrors,finishContact,prefillBarrier,sampleContact,type ContactDraft,type ContactField} from '@/lib/contact/model';
import {scenarios} from '@/lib/scenarios/manifest';
import type {DemoRoute} from '@/lib/routes';
export function ContactForm({route,seed,embedded=false}:{embedded?:boolean;route:DemoRoute;seed:{topic:'contact'|'barrier';path:string}}) {
const Root=embedded?'section':'main';const Heading=embedded?'h2':'h1';const PanelHeading=embedded?'h3':'h2';
const submitButton=useRef<HTMLButtonElement>(null);const completeButton=useRef<HTMLButtonElement>(null);const previousResult=useRef('idle');
const t=useTranslations('Contact');const f=useTranslations('Forms');const [state,setContact]=useDemoState('contact');const [errors,setErrors]=useState<ContactField[]>([]);const [pause,setPause]=useState(true);const summary=useRef<HTMLDivElement>(null);const focusErrors=useRef(false);const seeded=useRef(false);const improved=route.variant==='improved';
useEffect(()=>{if(state.result==='loading')completeButton.current?.focus();else if(previousResult.current==='loading')submitButton.current?.focus();previousResult.current=state.result;},[state.result]);
useEffect(()=>{if(seeded.current)return;seeded.current=true;if(seed.topic==='barrier'){const current=readState('contact');const next=prefillBarrier(current,seed.path);if(next!==current)setContact(next);}},[seed,setContact]);
useEffect(()=>{if(focusErrors.current){summary.current?.focus();focusErrors.current=false;}},[errors]);
useEffect(()=>{const reset=()=>{setErrors([]);setPause(true);};window.addEventListener('demo-reset',reset);return()=>window.removeEventListener('demo-reset',reset);},[]);
function update<K extends keyof ContactDraft>(field:K,value:ContactDraft[K]){const current=readState('contact');setContact({...current,draft:{...current.draft,[field]:value},result:'idle'});}
function finish(){setContact(finishContact(readState('contact')));}
const busy=state.result==='loading';const errorFor=(field:ContactField)=>errors.includes(field)?t(`errors.${field}`):undefined;
return <Root id={embedded?'contact-fixture':'main-content'} className={embedded?'embedded-fixture':'page-container purchase-page'}><header className="catalog-page-header"><Heading>{t('title')}</Heading><p>{t('intro')}</p></header><div className="account-layout"><section className="account-form-section" aria-label={t('formLabel')}>
{improved&&<FormErrors headingLevel={embedded?3:2} id="contact-error-summary" ref={summary} title={f('checkFields')} errors={errors.map(field=>({id:`contact-${field}`,text:t(`errors.${field}`)}))}/>}
<form className="demo-form" noValidate onSubmit={event=>{event.preventDefault();const current=readState('contact');const invalid=contactErrors(current.draft);focusErrors.current=improved&&invalid.length>0;setErrors(invalid);if(invalid.length){setContact({...current,result:'idle'});return;}const next=beginContact(current);setContact(pause?next:finishContact(next));}}>
<fieldset disabled={busy}><legend className="sr-only">{t('formLabel')}</legend><div className="form-field"><label htmlFor="contact-topic">{t('topic')}</label><select id="contact-topic" value={state.draft.topic} onChange={event=>{update('topic',event.target.value as 'contact'|'barrier');setErrors([]);}}><option value="contact">{t('general')}</option><option value="barrier">{t('barrier')}</option></select></div>
{(['name','email'] as const).map(field=><FormField key={field} id={`contact-${field}`} label={t(`fields.${field}`)} type={field==='email'?'email':'text'} value={state.draft[field]} onChange={event=>update(field,event.target.value)} maxLength={80} autoComplete="off" required error={improved?errorFor(field):undefined} className={errorFor(field)?'field-invalid':undefined}/>)}
<div id="contact-message-field" className="form-field"><label htmlFor="contact-message">{t('fields.message')}</label><textarea id="contact-message" value={state.draft.message} maxLength={1200} required aria-invalid={improved&&errors.includes('message')?true:undefined} aria-describedby={improved&&errors.includes('message')?'contact-message-error':'contact-message-help'} className={errorFor('message')?'field-invalid':undefined} onChange={event=>update('message',event.target.value)}/><p id="contact-message-help" className="field-help">{t('messageHelp')}</p>{improved&&errorFor('message')&&<p className="field-error" id="contact-message-error">{errorFor('message')}</p>}</div>
{state.draft.topic==='barrier'&&<><FormField id="contact-path" label={t('fields.path')} value={state.draft.path} maxLength={200} onChange={event=>update('path',event.target.value)} error={improved?errorFor('path'):undefined} className={errorFor('path')?'field-invalid':undefined}/><p className="field-help">{t('pathHelp')}</p><div className="form-field"><label htmlFor="contact-scenario">{t('fields.scenario')}</label><select id="contact-scenario" value={state.draft.scenario} onChange={event=>update('scenario',event.target.value)} aria-invalid={improved&&errors.includes('scenario')?true:undefined} aria-describedby={improved&&errors.includes('scenario')?'contact-scenario-error':undefined}><option value="">{t('noScenario')}</option>{scenarios.map(scenario=><option key={scenario.id} value={scenario.id}>{scenario.id}</option>)}</select>{improved&&errorFor('scenario')&&<p className="field-error" id="contact-scenario-error">{errorFor('scenario')}</p>}</div></>}
<Button type="button" variant="outline" size="sm" onClick={()=>{const current=readState('contact');setContact({...current,draft:{...current.draft,name:sampleContact.name,email:sampleContact.email,message:t('sampleMessage')},result:'idle'});setErrors([]);}}>{t('fillSample')}</Button></fieldset>
<fieldset disabled={busy} className="simulation-controls"><legend>{t('demoControls')}</legend><div className="form-field"><label htmlFor="contact-outcome">{t('outcome')}</label><select id="contact-outcome" value={state.choice} onChange={event=>setContact({...readState('contact'),choice:event.target.value as 'success'|'failure',result:'idle'})}><option value="success">{t('successChoice')}</option><option value="failure">{t('failureChoice')}</option></select></div><label className="check-label"><input type="checkbox" checked={pause} onChange={event=>setPause(event.target.checked)}/>{f('pause')}</label></fieldset>
{busy?<div className="submission-paused"><p>{f('paused')}</p><div className="form-actions"><Button ref={completeButton} type="button" onClick={finish}>{t('complete')}</Button><Button type="button" variant="outline" onClick={()=>setContact({...readState('contact'),result:'idle'})}>{t('cancel')}</Button></div></div>:<Button ref={submitButton} type="submit">{t(state.result==='failure'?'retry':'submit')}</Button>}
</form>
{/* contact-status: the barriers result intentionally has no live region. */}
<p id="contact-submit-status" role={improved?(state.result==='failure'?'alert':'status'):undefined} aria-atomic={improved?true:undefined} className={state.result==='failure'?'field-error':'form-status'}>{state.result==='idle'?'':t(`results.${state.result}`)}</p>
</section><aside className="demo-help"><PanelHeading>{t('demoTitle')}</PanelHeading><p>{t('demoCopy')}</p><p className="field-help">{t('privacy')}</p></aside></div></Root>;
}
src/components/shared/forms/form-field.tsx
'use client';
import type {ComponentProps,Ref} from 'react';
export function FormField({id,label,error,...props}:ComponentProps<'input'>&{id:string;label:string;error?:string}) {
return <div className="form-field"><label htmlFor={id}>{label}</label><input {...props} id={id} aria-invalid={error?true:undefined} aria-describedby={error?`${id}-error`:props['aria-describedby']}/>{error&&<p className="field-error" id={`${id}-error`}>{error}</p>}</div>;
}
export function FormErrors({id,errors,title,ref,headingLevel=2}:{headingLevel?:2|3;id:string;errors:{id:string;text:string}[];title:string;ref?:Ref<HTMLDivElement>}) {
const Heading=headingLevel===3?'h3':'h2';
if(!errors.length)return null;
return <div id={id} ref={ref} tabIndex={-1} className="error-summary"><Heading>{title}</Heading><ul>{errors.map(error=><li key={error.id}><a href={`#${error.id}`} onClick={event=>{event.preventDefault();document.getElementById(error.id)?.focus();}}>{error.text}</a></li>)}</ul></div>;
}
src/lib/contact/model.ts
import {z} from 'zod';
import {routeSchema} from '../routes';
import {findProduct} from '../fixtures/catalog';
import {scenarios} from '../scenarios/manifest';
export function isDemoReference(value:string) {
if(!value)return true;
if(value.length>200||/[?#%\\\s]/.test(value))return false;
const parts=value.split('/');if(!routeSchema.safeParse({locale:parts[1],version:parts[2],variant:parts[3]}).success)return false;
const suffix=parts.slice(4).join('/');return ['', 'products','cart','checkout','login','account/settings','contact','preferences'].includes(suffix)||/^products\/[a-z-]+$/.test(suffix)&&Boolean(findProduct(parts[5]));
}
export const contactFields=['name','email','message','path','scenario'] as const;
export type ContactField=typeof contactFields[number];
export const contactDraftSchema=z.object({topic:z.enum(['contact','barrier']),name:z.string().max(80),email:z.string().max(80),message:z.string().max(1200),path:z.string().max(200),scenario:z.string().max(80)}).strict();
export type ContactDraft=z.infer<typeof contactDraftSchema>;
export const emptyContact:ContactDraft={topic:'contact',name:'',email:'',message:'',path:'',scenario:''};
export const sampleContact:ContactDraft={...emptyContact,name:'Alex Beispiel',email:'alex@example.test',message:'Dies ist eine fiktive Nachricht für die Demo.'};
export const validContactSchema=contactDraftSchema.superRefine((data,ctx)=>{
const fail=(field:ContactField)=>ctx.addIssue({code:'custom',path:[field],message:field});
if(data.name.trim().length<2)fail('name');if(!z.email().safeParse(data.email).success)fail('email');if(data.message.trim().length<10)fail('message');
if(data.topic==='barrier'&&!isDemoReference(data.path))fail('path');
if(data.topic==='barrier'&&data.scenario&&!scenarios.some(scenario=>scenario.id===data.scenario))fail('scenario');
});
export function contactErrors(value:unknown):ContactField[]{const result=validContactSchema.safeParse(value);return result.success?[]:contactFields.filter(field=>result.error.issues.some(issue=>issue.path[0]===field));}
export const contactStateSchema=z.object({draft:contactDraftSchema,result:z.enum(['idle','loading','success','failure']),choice:z.enum(['success','failure'])}).strict().refine(state=>state.result==='idle'||validContactSchema.safeParse(state.draft).success,'Invalid submitted draft');
export type ContactState=z.infer<typeof contactStateSchema>;
export const initialContact:ContactState={draft:emptyContact,result:'idle',choice:'success'};
export function beginContact(state:ContactState):ContactState {
validContactSchema.parse(state.draft);return contactStateSchema.parse({...state,result:'loading'});
}
export function finishContact(state:ContactState):ContactState {
const current=contactStateSchema.parse(state);if(current.result!=='loading')throw new Error('Start a submission first');
validContactSchema.parse(current.draft);return {...current,result:current.choice};
}
export function prefillBarrier(state:ContactState,path:string):ContactState {
if(!isDemoReference(path))throw new Error('Unsupported demo path');
if(state.draft.topic==='barrier'&&state.draft.path===path)return state;
return contactStateSchema.parse({...state,draft:{...state.draft,topic:'barrier',path},result:'idle'});
}
src/app/[locale]/globals.css
.form-field .field-invalid {border:2px solid var(--destructive);}
.field-error {color:var(--destructive);font-size:.875rem;margin:.5rem 0 0;}
.error-summary {border:2px solid var(--destructive);padding:1.25rem;margin-top:1.5rem;}