Crypto Exchange is a single–page React application that allows a user to:
- Choose a source currency and amount
- Choose a target currency
- See the minimal exchange amount required for the selected pair
- Get a debounced estimate of the target amount
- Provide and validate an Ethereum address
- Submit the exchange request data via a typed callback
The UI is focused on a fast, keyboard‑friendly exchange flow with strong UX around validation and autocomplete behaviour. All exchange‑related data is fetched from the public ChangeNOW v2 API.
-
Exchange form (
ExchangeWrapper)- Controlled inputs for:
- Source amount (user editable, numeric)
- Target amount (read‑only, calculated)
- Ethereum address (validated)
- Currency selection via custom
Autocompletecomponents (for source and target). - Error handling and validation:
- Minimal amount per pair, fetched dynamically.
- “Pair disabled” handling if API returns
minAmount === nullortoAmount === null. - Visual error messages localized to from or to side of the form.
- Submit button state:
- Disabled when there is any validation error.
- Disabled while no minimal amount is known yet.
- Disabled while Ethereum address is empty or invalid.
- Callback‑driven submit:
- Optional
onSubmitprop receivesExchangeWrapperSubmitPropsTypewith:exchangeAmount,exchangeCurrencyrecieveAmount,recieveCurrencyethereumAddress
- If
onSubmitis not provided, the form prevents default browser submit and does not navigate or reload.
- Optional
- Controlled inputs for:
-
Exchange data hook (
useExchangeData)- Manages all domain state related to exchange:
currentExchangeOption– currently selected “from” currency.currentRecieveOption– currently selected “to” currency.ethereumAddress– Ethereum address with associated error string.allAvailableCurrencies– list of all currencies fetched from API.minExchangeAmount– minimal allowed amount for the current pair.
- Exposes business logic helpers:
reverseCurrencies()– swaps “from” and “to” currencies, preserving one side when possible.fetchEstimatedExchangeAmount(queries)– typed wrapper around the estimate endpoint.
- Performs side‑effectful data fetching:
- On mount, fetches
exchange/currenciesand validates the shape withisCurrencyItemTypeArray. - Whenever both currencies are chosen, fetches
exchange/min-amountand validates viaisMinExchangeAmountType.
- On mount, fetches
- Validates Ethereum address against
ETHEREUM_ADDRESS_REGEX(0xprefix + 40 hex chars) and exposes a human‑readable error ('invalid address').
- Manages all domain state related to exchange:
-
Autocomplete input (
Autocomplete)- Generic, typed autocomplete for currency‑like options:
- Option type extends
AutocompleteOptionProps{ ticker, name, image }. - Accepts
currency,setCurrency,options, and optionalinputProps.
- Option type extends
- UX and interaction model:
- Text input with debounced filtering of the options list (250 ms).
- Case‑insensitive matching by both
nameandticker. - Keyboard support:
ArrowUp/ArrowDownto move active option.Enterto select the active option.Spaceto open/close the options list.
- Mouse support:
- Click an option to select it.
- Click the icon button to toggle the list.
- Outside‑click behaviour:
- When closing via click outside, if the input content exactly matches an existing option (name or ticker), that option is auto‑selected.
- Otherwise the input is cleared and the full list is restored.
- Performance / UX details:
- Uses
useDebouncedCallbackfor delayed filtering. - Uses
useLayoutEffectto compute the dropdown’smaxHeightbased on viewport height. - Keeps options in a ref (
optionsRef) to avoid unnecessary re‑renders.
- Uses
- Generic, typed autocomplete for currency‑like options:
-
Autocomplete list (
AutocompleteList)- Renders the dropdown list for
Autocompletewith:- ARIA attributes:
role="listbox"for the list androle="option"for each item. - Visual highlighting for the active option.
- ARIA attributes:
- Implements progressive rendering:
- Shows items in batches (default 50) and extends the list in steps via
IntersectionObserverwatching a sentinel element (#options-loader) at the bottom.
- Shows items in batches (default 50) and extends the list in steps via
- Automatically scrolls to top and resets window when options change.
- Renders the dropdown list for
-
Icon button (
IconButton)- Simple, reusable button component to show icon‑only actions.
- Accepts:
icon(React node).fontSize,backgroundColor, and all common button HTML attributes (except customclassName, which is internal).
- Used by
Autocompleteto render open/close icons using the assets frompublic/icons.
-
Debounced callback hook (
useDebouncedCallback)- Custom hook (in
src/hooks/useDebouncedCallback.ts) used by bothExchangeWrapperandAutocompleteto debounce:- Exchange estimate recalculations.
- Autocomplete options filtering.
- Prevents excessive API calls and improves perceived responsiveness.
- Custom hook (in
-
API client (
exchangeFetch)- Located in
src/components/ExchangeWrapper/fetch/exchange-fetches.ts. - Wraps the browser
fetchAPI, targeting the ChangeNOW base URLhttps://api.changenow.io/v2/. - Adds a default
x-changenow-api-keyheader for all requests. - Supports query parameters (
queries), additionalRequestInit, and optional error hooks.
- Located in
-
Endpoints
exchange/currencies- Returns the full list of available currencies and networks.
- Mapped to
CurrencyItemTypeand validated withisCurrencyItemTypeArray.
exchange/min-amount- Accepts
fromCurrency,fromNetwork,toCurrency,toNetwork. - Returns a
MinExchangeAmountTypecontaining:minAmount– minimal source amount allowed.flow–'standart' | 'fixed-rate'.
- Accepts
exchange/estimated-amount- Accepts
fromCurrency,fromNetwork,toCurrency,toNetwork,fromAmount,flow, and an optionaltype('direct' | 'reverse'). - Returns
EstimatedExchangeAmountType(withtoAmount,validUntil, etc.), validated viaisEstimatedExchangeAmounType.
- Accepts
-
Type system (
src/types/api/types.ts)- Centralizes domain types for API communication:
CurrencyItemTypeMinExchangeAmountType,MinExchangeAmountQueriesEstimatedExchangeAmountType,EstimatedExchangeAmountQueries- Flow and type enums:
FlowExchangePropType,TypeExchangePropType
- Provides type guard functions to keep runtime data aligned with TypeScript types.
- Centralizes domain types for API communication:
Security note: The API key is currently stored in the client bundle. For production use, you should move sensitive keys to a secure backend or a proxy service and never expose them directly to the browser.
-
App shell (
App.tsx)- Renders a minimal shell:
<header>with"Crypto Exchange"title and subtitle.- Central
ExchangeWrapperform.
- Responsible for forwarding the optional
onSubmithandler intoExchangeWrapper.
- Renders a minimal shell:
-
Layout & styling
- Global styles in
src/styles/index.scssandsrc/styles/App.scss(resets, layout, colour tokens, input styles). - Fonts loaded from
src/fontsand imported insrc/index.tsx. - Feature‑scoped SCSS modules for:
ExchangeWrapper(index.module.scss)Autocomplete(index.module.scss)IconButton(index.module.scss)
- Global styles in
-
Accessibility
- Dropdowns and options use ARIA roles (
combobox,listbox,option) andaria-expanded,aria-controls. - Keyboard navigation for dropdowns and options.
- Disabled states for submit button and target amount input when the action is not valid.
- Dropdowns and options use ARIA roles (
src/App.tsx– Application shell and page layout.src/index.tsx– React entrypoint and global styles/fonts loading.src/components/ExchangeWrapperindex.tsx– Main exchange form UI and state orchestration.hooks/useExchangeData.ts– Business logic and API wiring.fetch/exchange-fetches.ts– Exchange API wrapper and endpoints.icons/swap.svg– Visual control to reverse currencies.
src/components/Autocompleteindex.tsx– Generic, debounced autocomplete input.AutocompleteList/index.tsx– Virtualized / lazy‑loaded list rendering.
src/components/IconButtonindex.tsx– Reusable icon button with styling.
src/hooks/useDebouncedCallback.ts– Debounce utility hook.src/typesapi/types.ts– API and domain models.scss/index.d.ts,svg/index.d.ts– Declarations for importing SCSS modules and SVGs.
src/styles– Theming, resets, layout, and component‑agnostic styles.
-
Install dependencies
npm install
-
Start development server
npm start
- Opens the app on
http://localhost:3000/(default Create React App behaviour). - Hot reloads on file changes.
- Opens the app on
-
Run tests
npm test- Uses Jest and React Testing Library (
@testing-library/react,@testing-library/jest-dom). - Unit tests cover:
ExchangeWrapperbehaviour (form state, validation, submit payload).useExchangeDatahook logic (API calls, error handling).AutocompleteandAutocompleteListinteractions (filtering, navigation, selection).
- Uses Jest and React Testing Library (
-
Production build
npm run build
- Creates an optimized production bundle in the
buildfolder.
- Creates an optimized production bundle in the
-
Handling actual exchanges
- Implement a real
onSubmithandler inApp.tsxto send the collected data to your backend or a third‑party service. - You can log, persist, or forward
ExchangeWrapperSubmitPropsTypesafely thanks to strong typing.
- Implement a real
-
Adding networks or extra fields
- Extend
CurrencyItemTypeor wrap it into your own type, then pass the richer objects touseExchangeDataandAutocomplete. - Add extra form fields below the existing ones in
ExchangeWrapperand include them in the submitted payload.
- Extend
-
Changing debounce timings
ExchangeWrapperusesDEFAULT_DELAY_MS = 500ms for estimation calls.AutocompleteusesDEFAULT_DELAY_MS = 250ms for filtering.- Both can be changed to tune perceived responsiveness vs API load.
-
Styling & theming
- Override colours and spacing in
src/styles/_colors.scssand related partials. - Adjust component‑specific layouts in each
index.module.scsswithout changing the TypeScript logic.
- Override colours and spacing in
- The API key is embedded in the client bundle; you should proxy requests through a backend service in a real production environment.
- Error handling is intentionally minimal and assumes the ChangeNOW API is generally available and well‑behaved.
- The app currently targets desktop‑first layout; additional tweaks may be required for a fully polished mobile experience.
- The form does not persist state across page reloads; if you need persistence, integrate local storage or a backend session.