diff --git a/readme.md b/readme.md index 1ff4bc95b4..8f5a853540 100644 --- a/readme.md +++ b/readme.md @@ -1,10 +1,34 @@ -# 99Tech Code Challenge #1 # +## Solutions (Frontend Engineer application) -Note that if you fork this repository, your responses may be publicly linked to this repo. -Please submit your application along with the solutions attached or linked. +Applying for the **Frontend Engineer** role, so Problems 1–3 were attempted; +Problems 4–5 are left as-is (out of scope for this role). -It is important that you minimally attempt the problems, even if you do not arrive at a working solution. +### [Problem 1 — Three ways to sum to n](src/problem1/sum_to_n.js) -## Submission ## -You can either provide a link to an online repository, attach the solution in your application, or whichever method you prefer. -We're cool as long as we can view your solution without any pain. +Three implementations (iterative loop, closed-form Gauss formula, recursive), +plus a `runTestCases()` function that checks all three against a shared set +of inputs, including large values where the recursive version is +intentionally skipped (it would overflow the call stack). + +```bash +node src/problem1/sum_to_n.js +``` + +### [Problem 2 — Fancy Form](src/problem2/) + +Currency swap form built with React + TypeScript + Vite. Uses live prices +from `interview.switcheo.com/prices.json` and token icons from +[Switcheo/token-icons](https://github.com/Switcheo/token-icons). See +[src/problem2/README.md](src/problem2/README.md) for details and how it's +structured. + +```bash +cd src/problem2 +npm install +npm run dev +``` + +### [Problem 3 — Messy React](src/problem3/analysis.md) + +Write-up of the inefficiencies/anti-patterns found in the given code block, +plus a refactored version with an explanation of each change. diff --git a/src/problem1/.keep b/src/problem1/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/problem1/sum_to_n.js b/src/problem1/sum_to_n.js new file mode 100644 index 0000000000..14737fc2be --- /dev/null +++ b/src/problem1/sum_to_n.js @@ -0,0 +1,82 @@ +/** + * Problem 1: Three ways to sum to n + * + * Input: n - any integer + * Assumption: the result is always < Number.MAX_SAFE_INTEGER. + * Output: sum of integers from 1 to n, e.g. sum_to_n(5) === 1+2+3+4+5 === 15. + * + * These implementations assume n >= 0, consistent with the "sum to n" spec + * (there is no natural summation from 1 to a negative n). + */ + +// A. Iterative loop — O(n) time, O(1) space. +// Most straightforward and easy to read; fine for small/medium n. +var sum_to_n_a = function (n) { + let sum = 0; + for (let i = 1; i <= n; i++) { + sum += i; + } + return sum; +}; + +// B. Mathematical formula (Gauss' sum) — O(1) time, O(1) space. +// The fastest option, no loop needed: n * (n + 1) / 2. +var sum_to_n_b = function (n) { + return (n * (n + 1)) / 2; +}; + +// C. Recursive — O(n) time, O(n) space (call stack). +// Demonstrates a functional/recursive approach. Not suitable for very large n +// (risks a stack overflow), but included as a distinct technique from A and B. +var sum_to_n_c = function (n) { + if (n <= 0) return 0; + return n + sum_to_n_c(n - 1); +}; + +// Runs a shared set of test cases through all three implementations and +// checks that they agree, so a regression in any one of them is obvious. +// +// sum_to_n_c is recursive, so it's only exercised for n small enough to fit +// the call stack; sum_to_n_a/b (loop/formula) are also checked against much +// larger n, where a naive recursive approach would overflow the stack. +function runTestCases() { + const recursionSafeCases = [0, 1, 5, 10, 100, 1000]; + const largeOnlyCases = [1_000_000, 12345678]; + let allPassed = true; + + for (const n of recursionSafeCases) { + const a = sum_to_n_a(n); + const b = sum_to_n_b(n); + const c = sum_to_n_c(n); + const expected = (n * (n + 1)) / 2; + const passed = a === expected && b === expected && c === expected; + allPassed = allPassed && passed; + + console.log( + `sum_to_n(${n}) => a=${a} b=${b} c=${c} expected=${expected} ${ + passed ? "PASS" : "FAIL" + }` + ); + } + + for (const n of largeOnlyCases) { + const a = sum_to_n_a(n); + const b = sum_to_n_b(n); + const expected = (n * (n + 1)) / 2; + const passed = a === expected && b === expected; + allPassed = allPassed && passed; + + console.log( + `sum_to_n(${n}) => a=${a} b=${b} expected=${expected} (c skipped: would overflow the call stack) ${ + passed ? "PASS" : "FAIL" + }` + ); + } + + console.log(allPassed ? "\nAll test cases passed." : "\nSome test cases failed."); + return allPassed; +} + +runTestCases(); + +module.exports = { sum_to_n_a, sum_to_n_b, sum_to_n_c, runTestCases }; diff --git a/src/problem2/.gitignore b/src/problem2/.gitignore new file mode 100644 index 0000000000..a547bf36d8 --- /dev/null +++ b/src/problem2/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/src/problem2/.oxlintrc.json b/src/problem2/.oxlintrc.json new file mode 100644 index 0000000000..6fa991dad2 --- /dev/null +++ b/src/problem2/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/src/problem2/README.md b/src/problem2/README.md new file mode 100644 index 0000000000..b2b93b0579 --- /dev/null +++ b/src/problem2/README.md @@ -0,0 +1,47 @@ +# Fancy Form — Currency Swap + +A currency swap form built with **React + TypeScript + Vite**. + +## Run it + +```bash +npm install +npm run dev +``` + +## What it does + +- Fetches live prices from `https://interview.switcheo.com/prices.json` on load. + The feed has duplicate/stale rows per currency, so only the latest, + positive-price entry per currency is kept. A bundled snapshot + (`src/prices.json`) is used as a fallback if the live fetch fails. +- Token icons come from [Switcheo/token-icons](https://github.com/Switcheo/token-icons), + copied into `public/tokens/`. Any currency without a matching icon falls + back to a circular badge with its initials (see `TokenIcon.tsx`). +- Amount to receive is derived automatically from the live exchange rate — + no separate "calculate" step. +- Input validation: the send amount must be a positive number, and the two + sides can't both be the same token (enforced by disabling that option in + the token picker). +- A ⇅ button reverses the swap direction, carrying over the current output + amount as the new input. +- Submitting simulates a backend call (`setTimeout`) so the loading spinner + on the submit button is visible, then shows a success message — there's no + real backend for this challenge. +- Token pickers are searchable dropdowns (type to filter by symbol). + +## Structure + +``` +src/ + components/ + SwapForm.tsx - presentational: renders the hook's state + TokenSelect.tsx - searchable token dropdown + TokenIcon.tsx - token icon with initials fallback + hooks/useSwapForm.ts - all form state, validation, and submit logic + utils/ + loadPrices.ts - fetch + dedupe the price feed + format.ts - number formatting + data/fallbackPrices.ts + prices.json - bundled fallback snapshot +``` diff --git a/src/problem2/index.html b/src/problem2/index.html index 4058a68bff..2cdfca048c 100644 --- a/src/problem2/index.html +++ b/src/problem2/index.html @@ -1,27 +1,13 @@ - - - - - Fancy Form - - - - - - - - -
-
Swap
- - - - - - - -
- - - + + + + + + + Fancy Swap + + +
+ + diff --git a/src/problem2/package-lock.json b/src/problem2/package-lock.json new file mode 100644 index 0000000000..4d0578f63f --- /dev/null +++ b/src/problem2/package-lock.json @@ -0,0 +1,1273 @@ +{ + "name": "problem2", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "problem2", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.8", + "react-dom": "^19.2.8" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.4", + "oxlint": "^1.75.0", + "typescript": "~6.0.2", + "vite": "^8.2.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz", + "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.78.0.tgz", + "integrity": "sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.78.0.tgz", + "integrity": "sha512-CDfxZgB61B7buRdY2FJoAYYPPXCZ1EoC1LKscnC5dg3kjobdxiconvAvvN1BmHyW4PyFT3jRLDag/BY/roSNBQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.78.0.tgz", + "integrity": "sha512-2Y2U9Ahrz+OO0Ej88f9SJYq51/jUBp1Mc7iZu0ukrbeeZ3gpRGfzIFnoqfHDY96xr0GEfNrPUBFEy0nN5aD7HA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.78.0.tgz", + "integrity": "sha512-rpych6eJq6m9jDRypTEaPD1xysaEW5h9+xuxhGK/QhOg+/xaqPZrCrTNoIl/f3nEjuJeCEmstNDlrE9rJi/3/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.78.0.tgz", + "integrity": "sha512-IcMGrQT3QizkOESUJd5et+rOhVqSkNDfNik1cvrKDqIbzqx9KMtRswpFgkCuNTSwylCFLKhGUu8KmqY1ZnC0Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.78.0.tgz", + "integrity": "sha512-/uLdoJ0IXE6vo/0f0LKjinQAp+re+VMaCWaNT8ENIv2EOCkSsc8SGaflXAuW0Jua2dq5+GLVWm1NQK7P3UFSNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.78.0.tgz", + "integrity": "sha512-7xi4Wb/O8NRJhLoUXmDJMUVpNYvB5kefdhFU1Jb8rtae4QoXlTiLwI14X4YvAXVZLNZChP8m5qO9SQAlWQTbkQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.78.0.tgz", + "integrity": "sha512-4hFW0+fVXa3OIh1Y4A5SPkmvI4wuuBSrCVKzOyE7PTjhc7yEqZ1pmvEEeS5Lj/MaqvegFxXyF33N+6jkehxdyg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.78.0.tgz", + "integrity": "sha512-oC0mvsgBJjlMijSDEhx9KuvR9zYeHXceA9MjbuXB1F8NSR78Yj2unOBrstEvTVaq+pko+kuue6DajC00eqvTdg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.78.0.tgz", + "integrity": "sha512-XAllT5SUZS+ohjuZ3/5S0cwe0r7eboiuigeStCZ5DXRYx/2KVM2UvQXvAfyzXEimtQjAB7cDQ2YxDe2Zl2WNQQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.78.0.tgz", + "integrity": "sha512-trucMER/0QtecoXvc1y/UVqE3kwJipDwrx4oHfj+nNm3dq2zjP44WT0CfHNDPM3G1DXIkx/gY6lAD21NSCZVhA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.78.0.tgz", + "integrity": "sha512-cm3O4F/HQbdzOUX5mKHqG5KDL6E5w0pnlZ+fbBy2rmLryPOowkuLagFHTopQsEIpjcaZoPOrL+BmmAytAG9HFg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.78.0.tgz", + "integrity": "sha512-33wRf6HqGNsybJ3qX4cGaQN2ODPxNmc1rMa0mrTmx3eFq1VzOnvQooi9bIGVYakW8a/wmqVx1mgsUm8R2xfTiw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.78.0.tgz", + "integrity": "sha512-rRdISSYegj6VganMZ9tjRjijowfHJ09IZU01i0toBAqr6n5LEtwHq2IeS4FjW2RoskOHlb6efB26H5izYb3GEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.78.0.tgz", + "integrity": "sha512-GmsP4rW0xTL6u5CVdcDsaN5Fbc7hBc382Wmar1kttbnwSEviM+rSINKOMQ+UQ6iH+AGwC+8gaAiwu134Tgh6Lg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.78.0.tgz", + "integrity": "sha512-sy9yeYuADc8a+n4TLBayzMCZiHPW78DcIFVpOXTmdKHWQeM9xe5uzkqIIZmi326D5hY9XVwacipEB1p7tQjPAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.78.0.tgz", + "integrity": "sha512-rjc2hF1KfMi8fZj1X/m3AmnHbdsF3rL0v6KQg0Uc880Yb2khjz+3U14sfdZ7jWTpRnN1m1NQa/TT7uU9lJWPrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.78.0.tgz", + "integrity": "sha512-zcuXFVrEFHIafRfkCQT8w/Xe41o07ozl/vwHq7p94vB29xVzsB0sZGYORU1jhcYKv3Lr0J3HbJ2T4fHH5rWmvA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.78.0.tgz", + "integrity": "sha512-Sb5ocmLSuYeOuXd+CFOToGKp/gjXUEWDnvIGwhnh8aq8wY4TMmEnKnvbogSW7RdMZv77JSARduS7/gv+khYEjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz", + "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz", + "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz", + "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz", + "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz", + "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz", + "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz", + "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz", + "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz", + "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz", + "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz", + "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz", + "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz", + "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz", + "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oxlint": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.78.0.tgz", + "integrity": "sha512-QgQePuxIqKOzo1KSjG2EnITEeWvWnKAm77eq8nrMtf6AGoA+zyGc4PFYtDNJSD25g/ibOwfQ851hZ4/SPkMVoA==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.78.0", + "@oxlint/binding-android-arm64": "1.78.0", + "@oxlint/binding-darwin-arm64": "1.78.0", + "@oxlint/binding-darwin-x64": "1.78.0", + "@oxlint/binding-freebsd-x64": "1.78.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.78.0", + "@oxlint/binding-linux-arm-musleabihf": "1.78.0", + "@oxlint/binding-linux-arm64-gnu": "1.78.0", + "@oxlint/binding-linux-arm64-musl": "1.78.0", + "@oxlint/binding-linux-ppc64-gnu": "1.78.0", + "@oxlint/binding-linux-riscv64-gnu": "1.78.0", + "@oxlint/binding-linux-riscv64-musl": "1.78.0", + "@oxlint/binding-linux-s390x-gnu": "1.78.0", + "@oxlint/binding-linux-x64-gnu": "1.78.0", + "@oxlint/binding-linux-x64-musl": "1.78.0", + "@oxlint/binding-openharmony-arm64": "1.78.0", + "@oxlint/binding-win32-arm64-msvc": "1.78.0", + "@oxlint/binding-win32-ia32-msvc": "1.78.0", + "@oxlint/binding-win32-x64-msvc": "1.78.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/rolldown": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz", + "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.144.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.4", + "@rolldown/binding-darwin-arm64": "1.2.4", + "@rolldown/binding-darwin-x64": "1.2.4", + "@rolldown/binding-freebsd-x64": "1.2.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", + "@rolldown/binding-linux-arm64-gnu": "1.2.4", + "@rolldown/binding-linux-arm64-musl": "1.2.4", + "@rolldown/binding-linux-ppc64-gnu": "1.2.4", + "@rolldown/binding-linux-s390x-gnu": "1.2.4", + "@rolldown/binding-linux-x64-gnu": "1.2.4", + "@rolldown/binding-linux-x64-musl": "1.2.4", + "@rolldown/binding-openharmony-arm64": "1.2.4", + "@rolldown/binding-win32-arm64-msvc": "1.2.4", + "@rolldown/binding-win32-x64-msvc": "1.2.4" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/src/problem2/package.json b/src/problem2/package.json new file mode 100644 index 0000000000..17465eecb3 --- /dev/null +++ b/src/problem2/package.json @@ -0,0 +1,25 @@ +{ + "name": "problem2", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.8", + "react-dom": "^19.2.8" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.4", + "oxlint": "^1.75.0", + "typescript": "~6.0.2", + "vite": "^8.2.0" + } +} diff --git a/src/problem2/public/favicon.svg b/src/problem2/public/favicon.svg new file mode 100644 index 0000000000..6893eb1323 --- /dev/null +++ b/src/problem2/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/problem2/public/tokens/ATOM.svg b/src/problem2/public/tokens/ATOM.svg new file mode 100644 index 0000000000..f3f9d10429 --- /dev/null +++ b/src/problem2/public/tokens/ATOM.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/problem2/public/tokens/BLUR.svg b/src/problem2/public/tokens/BLUR.svg new file mode 100644 index 0000000000..bf1e280984 --- /dev/null +++ b/src/problem2/public/tokens/BLUR.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/problem2/public/tokens/BUSD.svg b/src/problem2/public/tokens/BUSD.svg new file mode 100644 index 0000000000..b992764bd1 --- /dev/null +++ b/src/problem2/public/tokens/BUSD.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/problem2/public/tokens/ETH.svg b/src/problem2/public/tokens/ETH.svg new file mode 100644 index 0000000000..6a7cd5afc4 --- /dev/null +++ b/src/problem2/public/tokens/ETH.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/problem2/public/tokens/EVMOS.svg b/src/problem2/public/tokens/EVMOS.svg new file mode 100644 index 0000000000..8d0b90acb4 --- /dev/null +++ b/src/problem2/public/tokens/EVMOS.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/problem2/public/tokens/GMX.svg b/src/problem2/public/tokens/GMX.svg new file mode 100644 index 0000000000..80a8c19e74 --- /dev/null +++ b/src/problem2/public/tokens/GMX.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/problem2/public/tokens/IBCX.svg b/src/problem2/public/tokens/IBCX.svg new file mode 100644 index 0000000000..061c6ccefe --- /dev/null +++ b/src/problem2/public/tokens/IBCX.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/problem2/public/tokens/IRIS.svg b/src/problem2/public/tokens/IRIS.svg new file mode 100644 index 0000000000..59b13b5aa6 --- /dev/null +++ b/src/problem2/public/tokens/IRIS.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/problem2/public/tokens/KUJI.svg b/src/problem2/public/tokens/KUJI.svg new file mode 100644 index 0000000000..7c169dd724 --- /dev/null +++ b/src/problem2/public/tokens/KUJI.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/problem2/public/tokens/LSI.svg b/src/problem2/public/tokens/LSI.svg new file mode 100644 index 0000000000..48618f66b3 --- /dev/null +++ b/src/problem2/public/tokens/LSI.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/problem2/public/tokens/LUNA.svg b/src/problem2/public/tokens/LUNA.svg new file mode 100644 index 0000000000..6a839981dd --- /dev/null +++ b/src/problem2/public/tokens/LUNA.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/problem2/public/tokens/OKB.svg b/src/problem2/public/tokens/OKB.svg new file mode 100644 index 0000000000..786f75ba42 --- /dev/null +++ b/src/problem2/public/tokens/OKB.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/problem2/public/tokens/OKT.svg b/src/problem2/public/tokens/OKT.svg new file mode 100644 index 0000000000..0afcd8a1cf --- /dev/null +++ b/src/problem2/public/tokens/OKT.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/problem2/public/tokens/OSMO.svg b/src/problem2/public/tokens/OSMO.svg new file mode 100644 index 0000000000..e98545517b --- /dev/null +++ b/src/problem2/public/tokens/OSMO.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/problem2/public/tokens/RATOM.svg b/src/problem2/public/tokens/RATOM.svg new file mode 100644 index 0000000000..8cd5f8056f --- /dev/null +++ b/src/problem2/public/tokens/RATOM.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/problem2/public/tokens/STATOM.svg b/src/problem2/public/tokens/STATOM.svg new file mode 100644 index 0000000000..c3e505f8b3 --- /dev/null +++ b/src/problem2/public/tokens/STATOM.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/problem2/public/tokens/STEVMOS.svg b/src/problem2/public/tokens/STEVMOS.svg new file mode 100644 index 0000000000..a2a8888208 --- /dev/null +++ b/src/problem2/public/tokens/STEVMOS.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/problem2/public/tokens/STLUNA.svg b/src/problem2/public/tokens/STLUNA.svg new file mode 100644 index 0000000000..606be7947d --- /dev/null +++ b/src/problem2/public/tokens/STLUNA.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/problem2/public/tokens/STOSMO.svg b/src/problem2/public/tokens/STOSMO.svg new file mode 100644 index 0000000000..73d296046b --- /dev/null +++ b/src/problem2/public/tokens/STOSMO.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/problem2/public/tokens/STRD.svg b/src/problem2/public/tokens/STRD.svg new file mode 100644 index 0000000000..f25a48536c --- /dev/null +++ b/src/problem2/public/tokens/STRD.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/problem2/public/tokens/SWTH.svg b/src/problem2/public/tokens/SWTH.svg new file mode 100644 index 0000000000..353c1b5bcc --- /dev/null +++ b/src/problem2/public/tokens/SWTH.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/problem2/public/tokens/USC.svg b/src/problem2/public/tokens/USC.svg new file mode 100644 index 0000000000..32bd9541f5 --- /dev/null +++ b/src/problem2/public/tokens/USC.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/problem2/public/tokens/USD.svg b/src/problem2/public/tokens/USD.svg new file mode 100644 index 0000000000..abf67ab105 --- /dev/null +++ b/src/problem2/public/tokens/USD.svg @@ -0,0 +1,8 @@ + + + Token Symbol/USD + + + + + \ No newline at end of file diff --git a/src/problem2/public/tokens/USDC.svg b/src/problem2/public/tokens/USDC.svg new file mode 100644 index 0000000000..eee4251a7b --- /dev/null +++ b/src/problem2/public/tokens/USDC.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/problem2/public/tokens/WBTC.svg b/src/problem2/public/tokens/WBTC.svg new file mode 100644 index 0000000000..b597b84a4a --- /dev/null +++ b/src/problem2/public/tokens/WBTC.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/problem2/public/tokens/YieldUSD.svg b/src/problem2/public/tokens/YieldUSD.svg new file mode 100644 index 0000000000..e435a687c9 --- /dev/null +++ b/src/problem2/public/tokens/YieldUSD.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/problem2/public/tokens/ZIL.svg b/src/problem2/public/tokens/ZIL.svg new file mode 100644 index 0000000000..c89e8ce16f --- /dev/null +++ b/src/problem2/public/tokens/ZIL.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/problem2/public/tokens/ampLUNA.svg b/src/problem2/public/tokens/ampLUNA.svg new file mode 100644 index 0000000000..ebeab58687 --- /dev/null +++ b/src/problem2/public/tokens/ampLUNA.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/problem2/public/tokens/axlUSDC.svg b/src/problem2/public/tokens/axlUSDC.svg new file mode 100644 index 0000000000..fc879f679a --- /dev/null +++ b/src/problem2/public/tokens/axlUSDC.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/problem2/public/tokens/bNEO.svg b/src/problem2/public/tokens/bNEO.svg new file mode 100644 index 0000000000..561b2282d8 --- /dev/null +++ b/src/problem2/public/tokens/bNEO.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/problem2/public/tokens/rSWTH.svg b/src/problem2/public/tokens/rSWTH.svg new file mode 100644 index 0000000000..d5250ff90d --- /dev/null +++ b/src/problem2/public/tokens/rSWTH.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/problem2/public/tokens/wstETH.svg b/src/problem2/public/tokens/wstETH.svg new file mode 100644 index 0000000000..15c8a9716e --- /dev/null +++ b/src/problem2/public/tokens/wstETH.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/problem2/script.js b/src/problem2/script.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/problem2/src/App.css b/src/problem2/src/App.css new file mode 100644 index 0000000000..3fad0efb47 --- /dev/null +++ b/src/problem2/src/App.css @@ -0,0 +1,313 @@ +.page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; +} + +.swap-card { + width: 100%; + max-width: 420px; + background: var(--card-bg); + border-radius: 24px; + box-shadow: var(--shadow); + padding: 28px; + position: relative; +} + +.swap-card__title { + margin: 0 0 20px; + font-size: 1.4rem; + font-weight: 700; +} + +.swap-card__status { + font-size: 0.9rem; + color: var(--text-secondary); + margin: 0 0 16px; +} + +.swap-card__status--error { + color: var(--error); +} + +.swap-field { + background: var(--field-bg); + border-radius: 16px; + padding: 14px 16px; + transition: background-color 0.15s ease; +} + +.swap-field:focus-within { + background: var(--field-bg-hover); +} + +.swap-field + .swap-field { + margin-top: 4px; +} + +.swap-field__row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.swap-field__input { + flex: 1; + min-width: 0; + border: none; + background: transparent; + font-size: 1.6rem; + font-weight: 600; + color: var(--text-primary); + outline: none; +} + +.swap-field__input::placeholder { + color: var(--text-secondary); + opacity: 0.6; +} + +.swap-field__input:read-only { + color: var(--text-primary); +} + +.swap-flip { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + margin: -10px auto; + position: relative; + z-index: 1; + border-radius: 50%; + border: 4px solid var(--card-bg); + background: var(--field-bg); + color: var(--text-primary); + font-size: 1.1rem; + cursor: pointer; + transition: transform 0.15s ease, background-color 0.15s ease; +} + +.swap-flip:hover:not(:disabled) { + background: var(--field-bg-hover); + transform: rotate(180deg); +} + +.swap-flip:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +.swap-card__error { + color: var(--error); + font-size: 0.85rem; + margin: 10px 2px 0; +} + +.swap-card__rate { + color: var(--text-secondary); + font-size: 0.85rem; + margin: 12px 2px 0; +} + +.swap-card__success { + color: var(--success); + font-size: 0.85rem; + background: rgba(27, 138, 90, 0.1); + border-radius: 10px; + padding: 10px 12px; + margin: 14px 0 0; +} + +.swap-submit { + width: 100%; + margin-top: 20px; + padding: 14px; + border: none; + border-radius: 14px; + background: var(--accent); + color: #fff; + font-size: 1rem; + font-weight: 700; + letter-spacing: 0.02em; + cursor: pointer; + transition: background-color 0.15s ease, transform 0.1s ease; + display: flex; + align-items: center; + justify-content: center; + min-height: 50px; +} + +.swap-submit:hover:not(:disabled) { + background: var(--accent-hover); +} + +.swap-submit:active:not(:disabled) { + transform: scale(0.99); +} + +.swap-submit:disabled { + background: #c9c8e0; + cursor: not-allowed; +} + +.swap-submit__spinner { + width: 20px; + height: 20px; + border-radius: 50%; + border: 3px solid rgba(255, 255, 255, 0.4); + border-top-color: #fff; + animation: spin 0.7s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Token select */ +.token-select { + position: relative; + flex-shrink: 0; +} + +.token-select__label { + display: none; +} + +.token-select__trigger { + display: flex; + align-items: center; + gap: 8px; + border: none; + background: var(--card-bg); + border-radius: 999px; + padding: 6px 12px 6px 6px; + cursor: pointer; + font-weight: 600; + color: var(--text-primary); + box-shadow: 0 1px 2px rgba(20, 20, 43, 0.08); +} + +.token-select__trigger:hover { + background: var(--field-bg-hover); +} + +.token-select__value { + font-size: 0.95rem; +} + +.token-select__chevron { + font-size: 0.7rem; + color: var(--text-secondary); +} + +.token-select__dropdown { + position: absolute; + top: calc(100% + 8px); + right: 0; + width: 240px; + max-height: 320px; + background: var(--card-bg); + border: 1px solid var(--border); + border-radius: 16px; + box-shadow: var(--shadow); + padding: 10px; + z-index: 20; + display: flex; + flex-direction: column; +} + +.token-select__search { + border: 1px solid var(--border); + border-radius: 10px; + padding: 8px 10px; + font-size: 0.9rem; + margin-bottom: 8px; + outline: none; +} + +.token-select__search:focus { + border-color: var(--accent); +} + +.token-select__list { + list-style: none; + margin: 0; + padding: 0; + overflow-y: auto; + max-height: 250px; +} + +.token-select__empty { + padding: 10px; + text-align: center; + color: var(--text-secondary); + font-size: 0.85rem; +} + +.token-select__option { + width: 100%; + display: flex; + align-items: center; + gap: 10px; + border: none; + background: transparent; + padding: 8px 10px; + border-radius: 10px; + cursor: pointer; + font-size: 0.9rem; + color: var(--text-primary); + text-align: left; +} + +.token-select__option:hover:not(:disabled) { + background: var(--field-bg); +} + +.token-select__option:disabled { + opacity: 0.35; + cursor: not-allowed; +} + +.token-icon { + width: 28px; + height: 28px; + border-radius: 50%; + object-fit: contain; + flex-shrink: 0; + background: var(--field-bg); +} + +.token-icon--fallback { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background: var(--field-bg); + color: var(--text-secondary); + font-weight: 700; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg-gradient-1: #14131f; + --bg-gradient-2: #1c1830; + --card-bg: #211f34; + --field-bg: #2a273f; + --field-bg-hover: #34304c; + --text-primary: #f2f1fa; + --text-secondary: #9a97b8; + --border: #37334f; + --shadow: 0 20px 60px -20px rgba(0, 0, 0, 0.6); + } + + .token-select__trigger { + box-shadow: none; + } +} diff --git a/src/problem2/src/App.tsx b/src/problem2/src/App.tsx new file mode 100644 index 0000000000..83d7793d25 --- /dev/null +++ b/src/problem2/src/App.tsx @@ -0,0 +1,12 @@ +import { SwapForm } from "./components/SwapForm"; +import "./App.css"; + +function App() { + return ( +
+ +
+ ); +} + +export default App; diff --git a/src/problem2/src/components/SwapForm.tsx b/src/problem2/src/components/SwapForm.tsx new file mode 100644 index 0000000000..875e4f6863 --- /dev/null +++ b/src/problem2/src/components/SwapForm.tsx @@ -0,0 +1,113 @@ +import { useSwapForm } from "../hooks/useSwapForm"; +import { formatAmount } from "../utils/format"; +import { TokenSelect } from "./TokenSelect"; + +export function SwapForm() { + const { + status, + currencies, + fromCurrency, + toCurrency, + fromAmount, + toAmount, + rate, + error, + canSubmit, + submitting, + successMessage, + handleSwapDirection, + handleFromCurrencyChange, + handleToCurrencyChange, + handleAmountChange, + handleSubmit, + } = useSwapForm(); + + return ( +
+

Swap

+ + {status === "loading" && ( +

Loading token prices…

+ )} + {status === "error" && ( +

+ Couldn't load token prices. Please try again later. +

+ )} + +
+
+ handleAmountChange(e.target.value)} + disabled={status !== "ready"} + aria-label="Amount to send" + /> + +
+
+ + + +
+
+ + +
+
+ + {error &&

{error}

} + + {rate !== null && fromCurrency !== toCurrency && ( +

+ 1 {fromCurrency} ≈ {formatAmount(rate)} {toCurrency} +

+ )} + + + + {successMessage && ( +

+ {successMessage} +

+ )} +
+ ); +} diff --git a/src/problem2/src/components/TokenIcon.tsx b/src/problem2/src/components/TokenIcon.tsx new file mode 100644 index 0000000000..dbbc8dda30 --- /dev/null +++ b/src/problem2/src/components/TokenIcon.tsx @@ -0,0 +1,34 @@ +import { useState } from "react"; + +interface TokenIconProps { + currency: string; + size?: number; +} + +export function TokenIcon({ currency, size = 28 }: TokenIconProps) { + const [failed, setFailed] = useState(false); + + if (failed) { + return ( + + ); + } + + return ( + setFailed(true)} + /> + ); +} diff --git a/src/problem2/src/components/TokenSelect.tsx b/src/problem2/src/components/TokenSelect.tsx new file mode 100644 index 0000000000..b936044448 --- /dev/null +++ b/src/problem2/src/components/TokenSelect.tsx @@ -0,0 +1,101 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import { TokenIcon } from "./TokenIcon"; + +interface TokenSelectProps { + label: string; + currencies: string[]; + value: string; + onChange: (currency: string) => void; + disabledCurrency?: string; +} + +export function TokenSelect({ + label, + currencies, + value, + onChange, + disabledCurrency, +}: TokenSelectProps) { + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(""); + const rootRef = useRef(null); + + useEffect(() => { + function onClickOutside(e: MouseEvent) { + if (rootRef.current && !rootRef.current.contains(e.target as Node)) { + setOpen(false); + setQuery(""); + } + } + document.addEventListener("mousedown", onClickOutside); + return () => document.removeEventListener("mousedown", onClickOutside); + }, []); + + const filtered = useMemo(() => { + const q = query.trim().toLowerCase(); + if (!q) return currencies; + return currencies.filter((c) => c.toLowerCase().includes(q)); + }, [currencies, query]); + + function handleSelect(currency: string) { + if (currency === disabledCurrency) return; + onChange(currency); + setOpen(false); + setQuery(""); + } + + return ( +
+ {label} + + + {open && ( +
+ setQuery(e.target.value)} + /> +
    + {filtered.length === 0 && ( +
  • No tokens found
  • + )} + {filtered.map((currency) => { + const isDisabled = currency === disabledCurrency; + return ( +
  • + +
  • + ); + })} +
+
+ )} +
+ ); +} diff --git a/src/problem2/src/data/fallbackPrices.ts b/src/problem2/src/data/fallbackPrices.ts new file mode 100644 index 0000000000..9ba7b3636e --- /dev/null +++ b/src/problem2/src/data/fallbackPrices.ts @@ -0,0 +1,7 @@ +import type { TokenPrice } from "../types"; +import raw from "../prices.json"; + +// Bundled snapshot of https://interview.switcheo.com/prices.json, +// already deduplicated to the latest entry per currency. +// Used only if the live fetch at runtime fails (offline demo, CORS, etc). +export const fallbackPrices = raw as TokenPrice[]; diff --git a/src/problem2/src/hooks/useSwapForm.ts b/src/problem2/src/hooks/useSwapForm.ts new file mode 100644 index 0000000000..31cd5f0c40 --- /dev/null +++ b/src/problem2/src/hooks/useSwapForm.ts @@ -0,0 +1,137 @@ +import { useEffect, useMemo, useState } from "react"; +import type { PriceMap } from "../types"; +import { loadPrices } from "../utils/loadPrices"; +import { formatAmount } from "../utils/format"; + +const DEFAULT_FROM = "ETH"; +const DEFAULT_TO = "USDC"; + +type Status = "loading" | "ready" | "error"; + +export function useSwapForm() { + const [prices, setPrices] = useState({}); + const [status, setStatus] = useState("loading"); + + const [fromCurrency, setFromCurrency] = useState(DEFAULT_FROM); + const [toCurrency, setToCurrency] = useState(DEFAULT_TO); + const [fromAmount, setFromAmount] = useState(""); + + const [submitting, setSubmitting] = useState(false); + const [successMessage, setSuccessMessage] = useState(null); + + useEffect(() => { + let cancelled = false; + loadPrices() + .then((map) => { + if (cancelled) return; + setPrices(map); + setStatus("ready"); + // Fall back to whatever two currencies actually have prices, + // in case the defaults are ever missing from the feed. + const currencies = Object.keys(map); + if (!map[DEFAULT_FROM] || !map[DEFAULT_TO]) { + setFromCurrency(currencies[0] ?? ""); + setToCurrency(currencies[1] ?? currencies[0] ?? ""); + } + }) + .catch(() => { + if (!cancelled) setStatus("error"); + }); + return () => { + cancelled = true; + }; + }, []); + + const currencies = useMemo(() => Object.keys(prices).sort(), [prices]); + + const rate = useMemo(() => { + const fromPrice = prices[fromCurrency]?.price; + const toPrice = prices[toCurrency]?.price; + if (!fromPrice || !toPrice) return null; + return fromPrice / toPrice; + }, [prices, fromCurrency, toCurrency]); + + const parsedAmount = Number(fromAmount); + const hasAmount = fromAmount.trim().length > 0; + const isAmountValid = hasAmount && Number.isFinite(parsedAmount) && parsedAmount > 0; + + const toAmount = rate !== null && isAmountValid ? parsedAmount * rate : null; + + const error = useMemo(() => { + if (!hasAmount) return null; + if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) { + return "Enter an amount greater than 0."; + } + return null; + }, [hasAmount, parsedAmount]); + + const canSubmit = + status === "ready" && + isAmountValid && + rate !== null && + fromCurrency !== toCurrency && + !submitting; + + function handleSwapDirection() { + setFromCurrency(toCurrency); + setToCurrency(fromCurrency); + setFromAmount(toAmount !== null ? String(Number(toAmount.toFixed(6))) : ""); + setSuccessMessage(null); + } + + function handleFromCurrencyChange(currency: string) { + setFromCurrency(currency); + setSuccessMessage(null); + } + + function handleToCurrencyChange(currency: string) { + setToCurrency(currency); + setSuccessMessage(null); + } + + function handleAmountChange(value: string) { + // Allow only digits and a single decimal point while typing. + if (value !== "" && !/^\d*\.?\d*$/.test(value)) return; + setFromAmount(value); + setSuccessMessage(null); + } + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!canSubmit) return; + + setSubmitting(true); + setSuccessMessage(null); + + // No real backend for this challenge — simulate network latency so the + // loading state on the submit button is visible. + window.setTimeout(() => { + setSubmitting(false); + setSuccessMessage( + `Swapped ${formatAmount(parsedAmount)} ${fromCurrency} for ${formatAmount( + toAmount ?? 0 + )} ${toCurrency}.` + ); + setFromAmount(""); + }, 1200); + } + + return { + status, + currencies, + fromCurrency, + toCurrency, + fromAmount, + toAmount, + rate, + error, + canSubmit, + submitting, + successMessage, + handleSwapDirection, + handleFromCurrencyChange, + handleToCurrencyChange, + handleAmountChange, + handleSubmit, + }; +} diff --git a/src/problem2/src/index.css b/src/problem2/src/index.css new file mode 100644 index 0000000000..3a74dc10be --- /dev/null +++ b/src/problem2/src/index.css @@ -0,0 +1,34 @@ +:root { + color-scheme: light dark; + --bg-gradient-1: #eef1ff; + --bg-gradient-2: #f7f2ff; + --card-bg: #ffffff; + --field-bg: #f4f5fb; + --field-bg-hover: #ebedf7; + --text-primary: #1a1a2e; + --text-secondary: #6b6f8d; + --accent: #6c5ce7; + --accent-hover: #5a48d6; + --error: #e5484d; + --success: #1b8a5a; + --border: #e3e5f0; + --shadow: 0 20px 60px -20px rgba(60, 60, 120, 0.35); + + font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, sans-serif; +} + +* { + box-sizing: border-box; +} + +html, +body, +#root { + height: 100%; +} + +body { + margin: 0; + background: linear-gradient(160deg, var(--bg-gradient-1), var(--bg-gradient-2)); + color: var(--text-primary); +} diff --git a/src/problem2/src/main.tsx b/src/problem2/src/main.tsx new file mode 100644 index 0000000000..bef5202a32 --- /dev/null +++ b/src/problem2/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/src/problem2/src/prices.json b/src/problem2/src/prices.json new file mode 100644 index 0000000000..9f6a6b478f --- /dev/null +++ b/src/problem2/src/prices.json @@ -0,0 +1,162 @@ +[ + { + "currency": "ATOM", + "date": "2023-08-29T07:10:50.000Z", + "price": 7.186657333333334 + }, + { + "currency": "BLUR", + "date": "2023-08-29T07:10:40.000Z", + "price": 0.20811525423728813 + }, + { + "currency": "BUSD", + "date": "2023-08-29T07:10:40.000Z", + "price": 0.999183113 + }, + { + "currency": "ETH", + "date": "2023-08-29T07:10:52.000Z", + "price": 1645.9337373737374 + }, + { + "currency": "EVMOS", + "date": "2023-08-29T07:10:40.000Z", + "price": 0.06246181355932203 + }, + { + "currency": "GMX", + "date": "2023-08-29T07:10:40.000Z", + "price": 36.345114372881355 + }, + { + "currency": "IBCX", + "date": "2023-08-29T07:10:40.000Z", + "price": 41.26811355932203 + }, + { + "currency": "IRIS", + "date": "2023-08-29T07:10:40.000Z", + "price": 0.0177095593220339 + }, + { + "currency": "KUJI", + "date": "2023-08-29T07:10:45.000Z", + "price": 0.675 + }, + { + "currency": "LSI", + "date": "2023-08-29T07:10:50.000Z", + "price": 67.69661525423729 + }, + { + "currency": "LUNA", + "date": "2023-08-29T07:10:40.000Z", + "price": 0.40955638983050846 + }, + { + "currency": "OKB", + "date": "2023-08-29T07:10:40.000Z", + "price": 42.97562059322034 + }, + { + "currency": "OKT", + "date": "2023-08-29T07:10:40.000Z", + "price": 13.561577966101694 + }, + { + "currency": "OSMO", + "date": "2023-08-29T07:10:50.000Z", + "price": 0.3772974333333333 + }, + { + "currency": "RATOM", + "date": "2023-08-29T07:10:40.000Z", + "price": 10.250918915254237 + }, + { + "currency": "STATOM", + "date": "2023-08-29T07:10:45.000Z", + "price": 8.512162050847458 + }, + { + "currency": "STEVMOS", + "date": "2023-08-29T07:10:40.000Z", + "price": 0.07276706779661017 + }, + { + "currency": "STLUNA", + "date": "2023-08-29T07:10:40.000Z", + "price": 0.44232210169491526 + }, + { + "currency": "STOSMO", + "date": "2023-08-29T07:10:45.000Z", + "price": 0.431318 + }, + { + "currency": "STRD", + "date": "2023-08-29T07:10:40.000Z", + "price": 0.7386553389830508 + }, + { + "currency": "SWTH", + "date": "2023-08-29T07:10:45.000Z", + "price": 0.004039850455012084 + }, + { + "currency": "USC", + "date": "2023-08-29T07:10:40.000Z", + "price": 0.994 + }, + { + "currency": "USD", + "date": "2023-08-29T07:10:30.000Z", + "price": 1 + }, + { + "currency": "USDC", + "date": "2023-08-29T07:10:40.000Z", + "price": 0.989832 + }, + { + "currency": "WBTC", + "date": "2023-08-29T07:10:52.000Z", + "price": 26002.82202020202 + }, + { + "currency": "YieldUSD", + "date": "2023-08-29T07:10:40.000Z", + "price": 1.0290847966101695 + }, + { + "currency": "ZIL", + "date": "2023-08-29T07:10:50.000Z", + "price": 0.01651813559322034 + }, + { + "currency": "ampLUNA", + "date": "2023-08-29T07:10:40.000Z", + "price": 0.49548589830508477 + }, + { + "currency": "axlUSDC", + "date": "2023-08-29T07:10:40.000Z", + "price": 0.989832 + }, + { + "currency": "bNEO", + "date": "2023-08-29T07:10:50.000Z", + "price": 7.1282679 + }, + { + "currency": "rSWTH", + "date": "2023-08-29T07:10:40.000Z", + "price": 0.00408771 + }, + { + "currency": "wstETH", + "date": "2023-08-29T07:10:40.000Z", + "price": 1872.2579742372882 + } +] \ No newline at end of file diff --git a/src/problem2/src/types.ts b/src/problem2/src/types.ts new file mode 100644 index 0000000000..18750c5116 --- /dev/null +++ b/src/problem2/src/types.ts @@ -0,0 +1,7 @@ +export interface TokenPrice { + currency: string; + price: number; + date: string; +} + +export type PriceMap = Record; diff --git a/src/problem2/src/utils/format.ts b/src/problem2/src/utils/format.ts new file mode 100644 index 0000000000..0514b3a663 --- /dev/null +++ b/src/problem2/src/utils/format.ts @@ -0,0 +1,4 @@ +export function formatAmount(value: number): string { + if (!Number.isFinite(value)) return ""; + return value.toLocaleString(undefined, { maximumFractionDigits: 6 }); +} diff --git a/src/problem2/src/utils/loadPrices.ts b/src/problem2/src/utils/loadPrices.ts new file mode 100644 index 0000000000..99b3a1cad7 --- /dev/null +++ b/src/problem2/src/utils/loadPrices.ts @@ -0,0 +1,30 @@ +import type { PriceMap, TokenPrice } from "../types"; +import { fallbackPrices } from "../data/fallbackPrices"; + +const PRICES_URL = "https://interview.switcheo.com/prices.json"; + +// The feed contains duplicate/stale rows per currency (and a few zero/undefined +// prices). Keep only the most recent, positive-price entry for each currency. +function toLatestPriceMap(entries: TokenPrice[]): PriceMap { + const latest: PriceMap = {}; + for (const entry of entries) { + if (!entry.price || entry.price <= 0) continue; + const existing = latest[entry.currency]; + if (!existing || new Date(entry.date) > new Date(existing.date)) { + latest[entry.currency] = entry; + } + } + return latest; +} + +export async function loadPrices(): Promise { + try { + const res = await fetch(PRICES_URL); + if (!res.ok) throw new Error(`Price feed responded with ${res.status}`); + const data: TokenPrice[] = await res.json(); + return toLatestPriceMap(data); + } catch (err) { + console.warn("Falling back to bundled price snapshot:", err); + return toLatestPriceMap(fallbackPrices); + } +} diff --git a/src/problem2/style.css b/src/problem2/style.css deleted file mode 100644 index 915af91c72..0000000000 --- a/src/problem2/style.css +++ /dev/null @@ -1,8 +0,0 @@ -body { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - min-width: 360px; - font-family: Arial, Helvetica, sans-serif; -} diff --git a/src/problem2/tsconfig.app.json b/src/problem2/tsconfig.app.json new file mode 100644 index 0000000000..6830b6f759 --- /dev/null +++ b/src/problem2/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/src/problem2/tsconfig.json b/src/problem2/tsconfig.json new file mode 100644 index 0000000000..1ffef600d9 --- /dev/null +++ b/src/problem2/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/src/problem2/tsconfig.node.json b/src/problem2/tsconfig.node.json new file mode 100644 index 0000000000..8455dcbc2c --- /dev/null +++ b/src/problem2/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/src/problem2/vite.config.ts b/src/problem2/vite.config.ts new file mode 100644 index 0000000000..8b0f57b91a --- /dev/null +++ b/src/problem2/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/src/problem3/.keep b/src/problem3/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/problem3/analysis.md b/src/problem3/analysis.md new file mode 100644 index 0000000000..faf5b3d4ac --- /dev/null +++ b/src/problem3/analysis.md @@ -0,0 +1,170 @@ +# Problem 3: Messy React — Issues Found + +## Computational inefficiencies / anti-patterns + +1. **`lhsPriority` is undefined — the filter is broken (bug, not just inefficient).** + The filter callback reads `lhsPriority`, but only `balancePriority` was + declared. This throws a `ReferenceError` at runtime (or silently returns + `undefined`/falsy under loose settings), so the filter never behaves as + intended. + +2. **The filter logic is inverted relative to what it's named for.** + `if (balance.amount <= 0) return true` keeps balances with **zero or + negative** amount and drops everything with a positive amount. A wallet + page almost certainly wants to display balances the user actually holds + (`amount > 0`), so the condition is backwards. + +3. **`getPriority` takes `blockchain: any`.** + `any` throws away type safety for the one function driving the sort/filter + logic. A `Blockchain` union/string-literal type would let TypeScript catch + typos and missing cases at compile time. + +4. **`getPriority` is redefined on every render.** + It's a pure function of `blockchain` with no dependency on component state + or props, so it doesn't need to live inside the component — it's + recreated (a new function reference) on every render for no benefit and, + because it's not memoized, forces `useMemo`/`useCallback` consumers that + depend on it to also recompute needlessly if it were ever added as a + dependency. It also encodes blockchain priority as a hardcoded + switch — a lookup object (`Record`) is more scalable and + removes the fallthrough-prone `switch`. + +5. **`useMemo` dependency array includes `prices` but the computation never + uses `prices`.** + `sortedBalances` is derived only from `balances` and `getPriority`. + Including `prices` causes the (already expensive, unstable) filter+sort to + re-run every time prices update — e.g. on every price tick — even though + the result wouldn't change. This is wasted CPU work. + +6. **The `sort` comparator has no `return` for the equal-priority case.** + When `leftPriority === rightPriority`, the comparator falls off the end + and returns `undefined`. Modern engines tolerate this, but it's undefined + behavior per the spec and should return `0` explicitly for a stable, + correct sort. + +7. **`sortedBalances.filter/sort` is recomputed, then mapped twice + (`formattedBalances`, then `rows` from `sortedBalances` again).** + `formattedBalances` is computed but never used — `rows` maps over + `sortedBalances` again and recomputes `toFixed()` inline instead of reusing + `formattedBalances`. This is dead code plus a wasted pass over the array. + +8. **`rows` maps `sortedBalances: WalletBalance[]` but types the callback + parameter as `FormattedWalletBalance`.** + `WalletBalance` has no `formatted` field, so `balance.formatted` is + accessing a property TypeScript shouldn't allow — this only "works" + because `sortedBalances` is implicitly typed as `any[]` (a consequence of + issue #3) or because the annotation is simply wrong and the compiler isn't + catching it. Either way, types and runtime data are out of sync. + +9. **`getPriority(balance.blockchain)` is called once per item in the filter + and again per item in the sort comparator (up to ~2n log n calls) — and a + third time isn't needed but nothing is memoized.** + For a hot list (wallet balances re-rendering on every price change), it's + cheap to precompute `{ ...balance, priority: getPriority(balance.blockchain) }` + once and sort/filter on the cached value instead of recalculating. + +10. **`key={index}` on ``.** + Using the array index as the React key is an anti-pattern for a list that + can reorder (which this one explicitly does, via sorting) or have items + added/removed — React can misassign state/DOM across re-renders. A stable + identifier such as `balance.currency` (assuming currencies are unique per + wallet) should be used instead. + +11. **`WalletBalance` is missing the `blockchain` field used throughout.** + `getPriority(balance.blockchain)` and the sort/filter all reference + `balance.blockchain`, but the `WalletBalance` interface only declares + `currency` and `amount`. This compiles only because `blockchain` access + on an implicitly-`any` value is unchecked — fixing the `any` in + `getPriority` (issue #3) would surface this as a real type error that + needs a proper fix: adding `blockchain: string` to the interface. + +12. **`prices[balance.currency] * balance.amount` isn't guarded against a + missing price.** + If a currency isn't present in `prices`, this evaluates to `NaN` and + renders `usdValue={NaN}` silently instead of handling the missing-price + case explicitly (e.g. skip the row, or show a loading/placeholder state). + +## Refactored version + +```tsx +import { useMemo } from "react"; + +interface WalletBalance { + currency: string; + blockchain: string; + amount: number; +} + +interface FormattedWalletBalance extends WalletBalance { + formatted: string; + usdValue: number; +} + +interface Props extends BoxProps {} + +const BLOCKCHAIN_PRIORITY: Record = { + Osmosis: 100, + Ethereum: 50, + Arbitrum: 30, + Zilliqa: 20, + Neo: 20, +}; + +function getPriority(blockchain: string): number { + return BLOCKCHAIN_PRIORITY[blockchain] ?? -99; +} + +const WalletPage: React.FC = (props: Props) => { + const { children, ...rest } = props; + const balances = useWalletBalances(); + const prices = usePrices(); + + const formattedBalances = useMemo(() => { + return balances + .filter((balance) => getPriority(balance.blockchain) > -99 && balance.amount > 0) + .sort((lhs, rhs) => getPriority(rhs.blockchain) - getPriority(lhs.blockchain)) + .map((balance) => ({ + ...balance, + formatted: balance.amount.toFixed(2), + usdValue: (prices[balance.currency] ?? 0) * balance.amount, + })); + }, [balances, prices]); + + const rows = formattedBalances.map((balance) => ( + + )); + + return
{rows}
; +}; +``` + +### What changed and why + +- **Filter now keeps positive balances with a known priority** (fixes the + inverted/broken condition and the `lhsPriority` bug). +- **`getPriority` is a module-level function backed by a lookup object** — + no per-render recreation, no `switch` fallthrough risk, and it's typed + (`string -> number`) instead of `any`. +- **One combined `useMemo`** does filter → sort → format → attach `usdValue` + in a single pass over the array, replacing the three separate passes + (`sortedBalances`, `formattedBalances`, `rows`) and the dead + `formattedBalances` variable. +- **`useMemo` depends on exactly what it uses** (`balances`, `prices` — since + `usdValue` now genuinely depends on `prices`), so it only recomputes when + one of those actually changes. +- **The sort comparator always returns a number**, including the + equal-priority case. +- **`blockchain` is a declared field on `WalletBalance`**, and + `FormattedWalletBalance` extends it instead of redeclaring `currency`/ + `amount`, keeping the two interfaces in sync by construction. +- **`key={balance.currency}`** replaces `key={index}` for stable identity + across re-sorts. +- **Missing prices default to `0`** instead of silently producing `NaN`, + making the missing-price case an explicit, visible value rather than a + silent bug.