From f8baa43cd184f3f104e2b3db5d87f269865d93dc Mon Sep 17 00:00:00 2001 From: crazyminerparody Date: Wed, 29 Jul 2026 22:07:32 +0100 Subject: [PATCH] feat: add SlippageView component with status tracking and retry capability to quote client --- src/app/quote/Client.tsx | 22 ++++++++++-- src/app/quote/Slippage.test.tsx | 29 +++++++++++++++ src/app/quote/Slippage.tsx | 64 +++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 src/app/quote/Slippage.test.tsx create mode 100644 src/app/quote/Slippage.tsx diff --git a/src/app/quote/Client.tsx b/src/app/quote/Client.tsx index 735f692..b1ff801 100644 --- a/src/app/quote/Client.tsx +++ b/src/app/quote/Client.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { TextField } from '@/components/TextField'; +import { SlippageView } from './Slippage'; import { apiFetch, type ApiError } from '@/lib/apiClient'; import { formatQuoteAmountDisplay, formatQuoteRateDisplay } from '@/lib/format'; import { useLocalStorage } from '@/lib/useLocalStorage'; @@ -206,6 +207,13 @@ export default function QuoteClient() { } }; + // Retry wrapper for the quote request – re‑uses the existing submission logic. + const retryQuote = useCallback(() => { + // Create a synthetic submit event to trigger the same validation & request flow. + // The event type is cast to any to satisfy the FormEvent generic. + onSubmit(new Event('submit') as any); + }, [onSubmit]); + return (
- {quote && + {quote && ( (() => { const amountFmt = formatQuoteAmountDisplay(quote.amount); const rateFmt = formatQuoteRateDisplay(quote.estimated_rate); @@ -301,7 +309,17 @@ export default function QuoteClient() { ); - })()} + })() + )} + {/* Slippage status UI */} + {formError && (

{formError}

diff --git a/src/app/quote/Slippage.test.tsx b/src/app/quote/Slippage.test.tsx new file mode 100644 index 0000000..b5b631d --- /dev/null +++ b/src/app/quote/Slippage.test.tsx @@ -0,0 +1,29 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { SlippageView } from './Slippage'; + +describe('SlippageView', () => { + test('renders empty state', () => { + render(); + expect(screen.getByTestId('slippage-status')).toHaveTextContent('No slippage data'); + }); + + test('renders loading state', () => { + render(); + expect(screen.getByTestId('slippage-status')).toHaveTextContent('Calculating slippage…'); + }); + + test('renders success state with slippage', () => { + render(); + expect(screen.getByTestId('slippage-status')).toHaveTextContent('Slippage: 0.42%'); + }); + + test('renders error state with retry button', () => { + const onRetry = jest.fn(); + render(); + expect(screen.getByTestId('slippage-status')).toHaveTextContent('Error: Network error'); + const btn = screen.getByRole('button', { name: /try again/i }); + expect(btn).toBeInTheDocument(); + fireEvent.click(btn); + expect(onRetry).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/app/quote/Slippage.tsx b/src/app/quote/Slippage.tsx new file mode 100644 index 0000000..7496734 --- /dev/null +++ b/src/app/quote/Slippage.tsx @@ -0,0 +1,64 @@ +'use client'; + +import { memo } from 'react'; +import { Tooltip } from '@/components/Tooltip'; + +/** + * UI for displaying slippage information. + * + * - empty → No quote yet (initial load). + * - loading → Quote request in progress. + * - error → Quote request failed; user can retry. + * - success → Quote succeeded; slippage value is shown. + * + * All state changes are announced via `role="status"` and `aria-live="polite"` + * for assistive technology. + */ +export type SlippageStatus = 'loading' | 'empty' | 'error' | 'success'; + +type Props = { + /** Current status of the slippage view. */ + status: SlippageStatus; + /** Slippage string to display when `status === 'success'`. */ + slippage?: string; + /** Optional error message shown when `status === 'error'`. */ + errorMessage?: string; + /** Callback invoked when the user clicks “Try again”. */ + onRetry?: () => void; +}; + +function SlippageBase({ status, slippage, errorMessage, onRetry }: Props) { + // Accessible live region – always present, content changes with status. + const liveRegion = ( + + {status === 'empty' && 'No slippage data'} + {status === 'loading' && 'Calculating slippage…'} + {status === 'error' && `Error: ${errorMessage ?? 'Unable to calculate'}`} + {status === 'success' && `Slippage: ${slippage}`} + + ); + + // Render a tooltip‑styled badge for visual consistency. + const tooltip = ( + + {liveRegion} + + ); + + return ( +
+ {tooltip} + {status === 'error' && onRetry && ( + + )} +
+ ); +} + +export const SlippageView = memo(SlippageBase);