diff --git a/src/App.jsx b/src/App.jsx index 092a9b5..06c26d0 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -8,6 +8,16 @@ import './styles.css' // Session-scoped cache: repeated identical queries skip recomputation. const findRoutesCached = createCachedFindRoutes() +/** + * Root component. Owns all query/results state and wires the form to the + * routing engine to the results list — the routing engine itself has no + * React dependency (see routingEngine.js), so all state management lives + * here rather than in a hook. + * + * Phase 1 passes the mocked `mockAnchors` dataset into the engine on every + * search; Phase 2 replaces that with live-fetched anchor data at this same + * call site, without needing to change the engine. + */ export default function App() { const [routes, setRoutes] = useState([]) const [searched, setSearched] = useState(false) @@ -17,8 +27,18 @@ export default function App() { const currencies = availableCurrencies() - async function handleSubmit({ fromCurrency, toCurrency, amount }) { - if (loading) return + /** + * Runs the routing engine for the submitted query and updates all + * derived state. Routing-engine errors (e.g. an invalid `anchors` + * argument) are caught here and surfaced as a form-level error rather + * than crashing the app. + * + * @param {Object} query + * @param {string} query.fromCurrency + * @param {string} query.toCurrency + * @param {number} query.amount + */ + function handleSubmit({ fromCurrency, toCurrency, amount }) { setEngineError(null) setSearched(false) setLoading(true) diff --git a/src/components/RemittanceForm.jsx b/src/components/RemittanceForm.jsx index 4f1b1c9..51cfb1e 100644 --- a/src/components/RemittanceForm.jsx +++ b/src/components/RemittanceForm.jsx @@ -4,6 +4,12 @@ import { useState } from 'react' * Currency-pair + amount input. Currencies are populated from whatever * data source is passed in (mock in Phase 1, live in Phase 2) — this * component never hardcodes a currency list. + * + * @param {Object} props + * @param {string[]} props.currencies - Available currency codes to populate both selects. + * @param {(query: { fromCurrency: string, toCurrency: string, amount: number }) => void} props.onSubmit + * Called with a validated query once the form passes its own client-side + * checks (both currencies selected, currencies differ, amount > 0). */ export default function RemittanceForm({ currencies, onSubmit }) { const [fromCurrency, setFromCurrency] = useState(currencies[0] ?? '') @@ -11,6 +17,8 @@ export default function RemittanceForm({ currencies, onSubmit }) { const [amount, setAmount] = useState('100') const [error, setError] = useState(null) + // Swaps the two selected currencies in place, so a user correcting a + // reversed pair doesn't have to re-select both dropdowns. function handleSwap() { setFromCurrency(toCurrency) setToCurrency(fromCurrency) diff --git a/src/components/RouteCard.jsx b/src/components/RouteCard.jsx index b12c8cb..563011d 100644 --- a/src/components/RouteCard.jsx +++ b/src/components/RouteCard.jsx @@ -1,37 +1,16 @@ -import { useState } from 'react' - -function buildRouteDetailsText(route) { - const path = [route.hops[0].fromCurrency, ...route.hops.map((h) => h.toCurrency)].join(' → ') - const hopLines = route.hops.map( - (hop, index) => - `${index + 1}. ${hop.fromCurrency} → ${hop.toCurrency} via ${hop.anchorName} — ${hop.feePercent}% fee, ~${hop.estimatedMinutes} min` - ) - return [ - `Route: ${path}`, - `You receive: ${route.outputAmount}`, - `Total fee: ${route.totalFeePercent}%`, - `Est. time: ${route.totalEstimatedMinutes} min`, - 'Hops:', - ...hopLines, - ].join('\n') -} - -function writeClipboard(text) { - if (navigator.clipboard?.writeText) { - return navigator.clipboard.writeText(text) - } - const textarea = document.createElement('textarea') - textarea.value = text - textarea.setAttribute('readonly', '') - textarea.style.position = 'absolute' - textarea.style.left = '-9999px' - document.body.appendChild(textarea) - textarea.select() - document.execCommand('copy') - document.body.removeChild(textarea) - return Promise.resolve() -} - +/** + * Displays a single ranked route: its currency path, headline stats + * (output amount, total fee, estimated time), and an expandable per-hop + * breakdown. + * + * Note: `route.totalCost` (the engine's internal ranking score) is + * deliberately not displayed here — it's not a monetary amount, only a + * sort key used to order the route list. + * + * @param {Object} props + * @param {import('../engine/routingEngine').Route} props.route - The route to display. + * @param {number} props.rank - This route's 1-based position in the ranked list, for display (e.g. "#1"). + */ export default function RouteCard({ route, rank }) { const [copied, setCopied] = useState(false) const path = [route.hops[0].fromCurrency, ...route.hops.map((h) => h.toCurrency)].join(' → ') diff --git a/src/components/RouteList.jsx b/src/components/RouteList.jsx index 54b7c77..a93497f 100644 --- a/src/components/RouteList.jsx +++ b/src/components/RouteList.jsx @@ -1,5 +1,17 @@ import RouteCard from './RouteCard' +/** + * Renders the ranked list of routes returned by the routing engine, or an + * appropriate empty/no-results state. + * + * Renders nothing at all before a search has run (`searched === false`), + * rather than an empty list, so the UI doesn't imply "no routes exist" + * before the user has actually searched. + * + * @param {Object} props + * @param {import('../engine/routingEngine').Route[]} props.routes - Ranked routes to display, cheapest-first. + * @param {boolean} props.searched - Whether a search has been run yet this session. + */ export default function RouteList({ routes, searched }) { if (!searched) { return null