This page uses a set of reusable components to present contract metadata, milestone progress, and context-aware actions. The page implements loading and error states via skeleton placeholders and error messaging wired to ActionPanel.
Props:
contractName: stringparties: { label: string; address: string }[]totalValue: numbercurrency: stringstatus: 'Active' | 'Completed' | 'Disputed' | 'Pending'createdAt: stringmilestoneCount: number
Description: Displays the contract name, current status badge, total value, creation date, and key parties with middle-truncated addresses.
Props:
milestones: Milestone[]
Description: Derives escrow metrics directly from the contract's milestone array and renders an accessible progress panel with a role="progressbar" indicator and paid/outstanding fund cards. Currency is taken from the milestones themselves — no value is hardcoded on the page. An empty milestones array renders a safe zero-state (0 / 0, 0% progress) without throwing.
The component is placed between ContractSummary and MilestonesList in the left column and wrapped in its own SafeBoundary. During data loading a ContractProgressSkeleton is shown in its place.
See docs/components/ContractProgress.md for the full data-calculation spec and ARIA attribute table.
Props:
milestones: Array<{ id: string; title: string; status: 'Pending' | 'Completed' | 'Paid' | 'Disputed'; payout: number; currency: string; dueDate?: string; }>contractCurrency?: string— optional contract-level currency. When provided, milestones whose currency differs (case-insensitive) trigger an accessiblerole="alert"warning banner near the milestones list identifying which and how many milestones mismatch.
Description: Renders a scrollable milestone roster, each showing the title, due date, status, and payout amount. When contractCurrency is provided, the component uses findCurrencyMismatches from src/lib/currencyMismatch.ts to detect and surface currency mismatches.
Props:
status: 'Active' | 'Completed' | 'Disputed' | 'Pending'onSubmitMilestone?: () => voidonDispute?: () => voidonReleaseFunds?: () => voidonViewSummary?: () => voiddisabledReasons?: Partial<Record<ActionKey, string>>errorMessage?: stringisLoading?: boolean
Description: Chooses appropriate action buttons based on the current contract status. See docs/components/ActionPanel.md for keyboard support, disabled-state reasons, loading, and error guidance.
Description: Renders a placeholder skeleton for ContractSummary while contract data is loading. Uses aria-busy="true" and aria-label="Loading contract summary" for accessibility announcement.
Description: Renders a placeholder skeleton for ContractProgress while contract data is loading. Uses aria-busy="true" and aria-label="Loading escrow progress" for accessibility announcement. Mirrors the visual shape of ContractProgress with pulsing grey blocks for the progress bar and both fund cards.
Description: Renders a placeholder skeleton for MilestonesList while milestones are loading. Uses aria-busy="true" and aria-label="Loading milestones" for accessibility announcement.
The resolveContractData function (in src/lib/contractResolver.ts) provides a typed, deterministic async interface for contract data. It accepts an optional config object with simulateError and simulateDelay flags for testing.
export async function resolveContractData(
id: string,
options: ResolverOptions = {}
): Promise<ContractData>In production, replace the mock implementation with a real API call. The return type is ContractData, which includes all fields needed by ContractSummary, MilestonesList, and ActionPanel.
- Loading: While data is resolving, skeleton placeholders display for
ContractSummaryandMilestonesList.ActionPanelreceivesisLoading={true}, which disables all buttons and announces a reason to screen readers. - Error: If data resolution fails,
ActionPaneldisplays an error message withrole="alert". Buttons remain disabled. Components are wrapped inSafeBoundaryto catch render errors.
- Update the
ActionPanelPropstype to include the callback for the new action. - Extend the
getActionButtonshelper insideActionPanel.tsxwith the new status-to-action mapping. - Add a new button render block in
ActionPanelthat uses the callback and descriptivearia-label. - Add unit tests in
src/components/__tests__/ActionPanel.test.tsxto verify the new action appears for the correct status and that the callback triggers.
The id route parameter is validated by isValidContractId (defined in src/lib/validateContractId.ts) before it is used anywhere on the page.
Rules enforced:
- Non-empty — an empty string is rejected.
- Allowed charset — only alphanumeric characters (
a–z,A–Z,0–9), hyphens (-), and underscores (_) are accepted. Slashes, angle brackets, null bytes, and other special characters are all rejected. - Max length — at most 64 characters. Oversized values are rejected.
If the id fails any rule, Next.js notFound() is called immediately and the existing not-found UI is shown. The raw param value is never rendered or forwarded.
The contract detail page uses a responsive grid:
- Desktop: a two-column layout with summary, escrow progress, and milestones on the left, and a sticky action panel on the right.
- Mobile: stacked content to keep text readable and controls accessible.
Left column order (top → bottom):
ContractSummary— contract name, status, total value, partiesContractProgress— escrow progress bar, paid/outstanding fund cardsMilestonesList— scrollable per-milestone detail rows
- Status badges use high contrast color combinations.
- Buttons include descriptive
aria-labelattributes, visible focus rings, and disabled-state descriptions. - Section headers use semantic landmarks and visible labels.