Fictional demo shop with intentional accessibility barriers. No real purchases.

Back to the demo shop

Standalone example · v1

A checkout in three steps.

Use the actual shop checkout to compare validation, step focus and deterministic mock results.

Prepare replaces the cart with two notebook sets: four physical items, €48. It clears the checkout address/result without logging you in or out.

Back to cart

Your demo checkout.

A simulated purchase in three steps. Use fictional details only. Nothing is sent or charged.

Your cart is empty.

Back to the collection
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/checkout/checkout-page.tsx

'use client';
import {useEffect,useRef,useState} from 'react';
import Link from 'next/link';
import {useTranslations} from 'next-intl';
import {Button} from '@/components/ui/button';
import {cartPath,catalogPath,type DemoRoute} from '@/lib/routes';
import {products} from '@/lib/fixtures/catalog';
import {cartTotalMinor,cartSignature} from '@/lib/cart/model';
import {formatPrice} from '@/lib/format';
import {addressFields,addressErrors,demoAddress,reviewCheckout,completeCheckout,editCheckout,outcomeSchema,type AddressField} from '@/lib/checkout/model';
import {useDemoState} from '@/lib/state/use-demo-state';
import {readState} from '@/lib/state/registry';

export function CheckoutPage({route,embedded=false}: {route:DemoRoute;embedded?:boolean}) {
 const Root=embedded?'section':'main';const Heading=embedded?'h2':'h1';const StepHeading=embedded?'h3':'h2';const DetailHeading=embedded?'h4':'h3';
 const t=useTranslations();const [cart]=useDemoState('cart');const [state,setCheckout]=useDemoState('checkout');const [errors,setErrors]=useState<AddressField[]>([]);
 const heading=useRef<HTMLHeadingElement>(null);const summary=useRef<HTMLDivElement>(null);const moveFocus=useRef(false);const improved=route.variant==='improved';
 useEffect(()=>{const reset=()=>{setErrors([]);moveFocus.current=false;};window.addEventListener('demo-reset',reset);return()=>window.removeEventListener('demo-reset',reset);},[]);
 useEffect(()=>{if(moveFocus.current){(errors.length?summary.current:heading.current)?.focus();moveFocus.current=false;}},[state.step,errors]);
 function focusNext() {moveFocus.current=improved;}
 const changed=state.step==='review' && cartSignature(cart)!==cartSignature(state.basket);
 function backToAddress() {focusNext();setErrors([]);setCheckout(editCheckout(readState('checkout')));}
 const summaryCart=state.step==='address'||changed?cart:state.basket;
 const empty=!cart.items.length && state.step!=='outcome';
 return <Root id={embedded?'checkout-fixture':'main-content'} className={embedded?'embedded-fixture':'page-container purchase-page'}><header className="catalog-page-header"><Link href={cartPath(route)}>{t('Checkout.backToCart')}</Link><Heading>{t('Checkout.title')}</Heading><p>{t('Checkout.intro')}</p></header>
 {empty?<section className="cart-empty"><StepHeading>{t('Cart.empty')}</StepHeading><Link href={catalogPath(route)}>{t('Catalog.backToCollection')}</Link></section>:<>
 <ol className="checkout-steps" aria-label={t('Checkout.steps')}>{(['address','review','outcome'] as const).map((step,index)=><li key={step} aria-current={state.step===step?'step':undefined}><span aria-hidden="true">{index+1}. </span>{t(`Checkout.${step}`)}</li>)}</ol>
 <div className="checkout-layout"><section className="checkout-main" aria-labelledby="checkout-step-heading"><StepHeading id="checkout-step-heading" tabIndex={-1} ref={heading}>{t(state.step==='outcome'?`Checkout.results.${state.outcome}.title`:`Checkout.${state.step}`)}</StepHeading>
 {state.step==='address'&&<form noValidate onSubmit={event=>{event.preventDefault();const current=readState('checkout');const invalid=addressErrors(current.address);focusNext();setErrors(invalid);if(!invalid.length){try{setCheckout(reviewCheckout(current,readState('cart')));}catch{moveFocus.current=false;}}}}>
 <p id="checkout-address-help" className="field-help">{t('Checkout.addressHelp')}</p><Button type="button" variant="outline" size="sm" onClick={()=>{setCheckout({...readState('checkout'),address:demoAddress});setErrors([]);}}>{t('Checkout.fillDemo')}</Button>
 {/* checkout-validation: barriers deliberately uses borders alone; labels/recovery stay intact. */}
 {improved&&errors.length>0&&<div id="checkout-error-summary" ref={summary} tabIndex={-1} className="error-summary"><DetailHeading>{t('Checkout.errorTitle')}</DetailHeading><ul>{errors.map(field=><li key={field}><a href={`#checkout-${field}`} onClick={event=>{event.preventDefault();document.getElementById(`checkout-${field}`)?.focus();}}>{t(`Checkout.errors.${field}`)}</a></li>)}</ul></div>}
 <div id="checkout-address-fields" className="address-fields" aria-describedby="checkout-address-help">{addressFields.map(field=><div key={field} className="address-field"><label htmlFor={`checkout-${field}`}>{t(`Checkout.fields.${field}`)}</label><input id={`checkout-${field}`} name={field} type={field==='email'?'email':'text'} inputMode={field==='postalCode'?'numeric':undefined} maxLength={field==='postalCode'?20:120} autoComplete="off" required value={state.address[field]} aria-invalid={improved&&errors.includes(field)?true:undefined} aria-describedby={improved&&errors.includes(field)?`checkout-${field}-error`:undefined} className={errors.includes(field)?'field-invalid':undefined} onChange={event=>setCheckout({...readState('checkout'),address:{...readState('checkout').address,[field]:event.target.value}})}/>{improved&&errors.includes(field)&&<p className="field-error" id={`checkout-${field}-error`}>{t(`Checkout.errors.${field}`)}</p>}</div>)}</div><p className="field-help">{t('Checkout.country')}</p><Button type="submit">{t('Checkout.toReview')}</Button></form>}
 {state.step==='review'&&(changed?<div role="status"><p>{t('Checkout.cartChanged')}</p><Button onClick={backToAddress}>{t('Checkout.editAddress')}</Button></div>:<><div className="review-address"><DetailHeading>{t('Checkout.address')}</DetailHeading><p>{state.address.name}<br/>{state.address.street}<br/>{state.address.postalCode} {state.address.city}<br/>{t('Checkout.germany')}<br/>{state.address.email}</p><Button variant="outline" onClick={backToAddress}>{t('Checkout.editAddress')}</Button></div><label className="outcome-control" htmlFor="checkout-outcome">{t('Checkout.chooseOutcome')}<select id="checkout-outcome" value={state.choice} onChange={event=>setCheckout({...readState('checkout'),choice:outcomeSchema.parse(event.target.value)})}>{outcomeSchema.options.map(choice=><option value={choice} key={choice}>{t(`Checkout.choices.${choice}`)}</option>)}</select></label><p className="field-help">{t('Checkout.outcomeHelp')}</p><div className="checkout-actions"><Button onClick={()=>{try{const next=completeCheckout(readState('checkout'),readState('cart'));focusNext();setCheckout(next);}catch{backToAddress();}}}>{t('Checkout.simulate')}</Button><Button variant="outline" onClick={()=>{const current=readState('checkout');focusNext();setCheckout(completeCheckout({...current,choice:'cancel'},readState('cart')));}}>{t('Checkout.cancel')}</Button></div></>)}
 {state.step==='outcome'&&<div id="checkout-result"><p>{t(`Checkout.results.${state.outcome}.copy`)}</p>{state.outcome==='success'&&<p className="mock-reference">{t('Checkout.reference',{reference:'DEMO-V1-001'})}</p>}<p className="field-help">{t('Checkout.retained')}</p><div className="checkout-actions"><Button onClick={()=>{const current=readState('checkout');focusNext();setErrors([]);if(readState('cart').items.length)setCheckout(reviewCheckout(current,readState('cart')));else setCheckout(editCheckout(current));}}>{t(state.outcome==='success'?'Checkout.runAgain':'Checkout.retry')}</Button><Button variant="outline" onClick={backToAddress}>{t('Checkout.editAddress')}</Button></div></div>}
 </section><aside className="checkout-summary" aria-label={t('Checkout.summary')}><StepHeading>{t('Checkout.summary')}</StepHeading><ul>{summaryCart.items.map(line=>{const product=products.find(product=>product.id===line.productId)!;const variant=product.variants.find(variant=>variant.id===line.variantId)!;return <li key={`${line.productId}-${line.variantId}`}><span>{product.name[route.locale]}<br/><span className="field-help">{t(`Catalog.${line.variantId}`)} × {line.quantity}</span></span><strong>{formatPrice(variant.priceMinor*line.quantity,route.locale)}</strong></li>;})}</ul><div className="cart-total"><span>{t('Cart.total')}</span><strong>{formatPrice(cartTotalMinor(summaryCart),route.locale)}</strong></div><p className="field-help">{t('Cart.demoNote')}</p></aside></div>
 </>}
 </Root>;
}

src/lib/checkout/model.ts

import {z} from 'zod';
import {cartSchema,cartSignature,type CartState} from '../cart/model';
export const addressFields = ['name','email','street','postalCode','city'] as const;
export type AddressField = typeof addressFields[number];
const boundedText = z.string().max(120);
export const addressDraftSchema = z.object({name:boundedText,email:boundedText,street:boundedText,postalCode:z.string().max(20),city:boundedText}).strict();
export type Address = z.infer<typeof addressDraftSchema>;
export const emptyAddress: Address = {name:'',email:'',street:'',postalCode:'',city:''};
export const demoAddress: Address = {name:'Alex Beispiel',email:'alex@example.test',street:'Musterweg 12',postalCode:'10115',city:'Berlin'};
const validAddressSchema = addressDraftSchema.extend({name:boundedText.trim().min(2),email:z.email().max(120),street:boundedText.trim().min(3),postalCode:z.string().regex(/^[0-9]{5}$/),city:boundedText.trim().min(2)});
export function addressErrors(address: Address): AddressField[] {
  const parsed=validAddressSchema.safeParse(address);
  return parsed.success ? [] : addressFields.filter(field=>parsed.error.issues.some(issue=>issue.path[0]===field));
}
export const outcomeSchema = z.enum(['success','failure','cancel']);
export type Outcome = z.infer<typeof outcomeSchema>;
const checkoutBaseSchema = z.object({address:addressDraftSchema,step:z.enum(['address','review','outcome']),choice:outcomeSchema,outcome:outcomeSchema.nullable(),basket:cartSchema}).strict();
export const checkoutSchema = checkoutBaseSchema.refine(state=>state.step==='address' ? state.outcome===null : addressErrors(state.address).length===0 && state.basket.items.length>0 && (state.step==='review' ? state.outcome===null : state.outcome===state.choice),'Inconsistent checkout state');
export type CheckoutState = z.infer<typeof checkoutSchema>;
export const initialCheckout: CheckoutState = {address:emptyAddress,step:'address',choice:'success',outcome:null,basket:{items:[]}};
export function reviewCheckout(state: CheckoutState, cart: CartState): CheckoutState {
  return checkoutSchema.parse({...state,step:'review',outcome:null,basket:cartSchema.parse(cart)});
}
export function completeCheckout(state: CheckoutState, cart: CartState): CheckoutState {
  const current=checkoutSchema.parse(state);
  if (current.step!=='review' || !cart.items.length || cartSignature(cart)!==cartSignature(current.basket)) throw new Error('Review the current cart first');
  return checkoutSchema.parse({...current,step:'outcome',outcome:current.choice});
}
export function editCheckout(state: CheckoutState): CheckoutState {return checkoutSchema.parse({...state,step:'address',outcome:null,basket:{items:[]}});}

src/app/[locale]/globals.css

.address-field .field-invalid {border:2px solid var(--destructive);}
.error-summary {border:2px solid var(--destructive);padding:1.25rem;margin-top:1.5rem;}