From b4609da5fa354f68c08d41782394f3f938543fa4 Mon Sep 17 00:00:00 2001 From: 0367592801 Date: Sat, 30 May 2026 16:59:37 +0700 Subject: [PATCH 1/2] feat: solve code challenge problems 1-3 - Problem 1: sumToN with 3 implementations + tests - Problem 2: token swap form with balances, formatting, icons - Problem 3: refactored React component with analysis notes Co-Authored-By: Claude Opus 4.7 --- .gitignore | 7 + assets/solution2-preview.png | Bin 0 -> 47969 bytes index.html | 12 + package-lock.json | 2135 +++++++++++++++++++++++ package.json | 27 + readme.md | 445 ++++- src/App.tsx | 80 + src/main.tsx | 10 + src/problem1/.keep | 0 src/problem1/lib/bigNumber.ts | 129 ++ src/problem1/lib/sumToN.ts | 54 + src/problem1/problem1.md | 31 + src/problem1/solutions.ts | 146 ++ src/problem1/style.css | 283 +++ src/problem1/tests/sumToN.test.ts | 149 ++ src/problem1/view.tsx | 336 ++++ src/problem2/api.ts | 40 + src/problem2/balances.ts | 62 + src/problem2/components/TokenIcon.tsx | 51 + src/problem2/components/TokenSelect.tsx | 141 ++ src/problem2/data/balances.json | 30 + src/problem2/format.ts | 26 + src/problem2/index.html | 27 - src/problem2/problem2.md | 31 + src/problem2/script.js | 0 src/problem2/style.css | 459 ++++- src/problem2/tokenIcon.ts | 9 + src/problem2/types.ts | 23 + src/problem2/view.tsx | 419 +++++ src/problem3/.keep | 0 src/problem3/Refactored.tsx | 73 + src/problem3/analysis.ts | 200 +++ src/problem3/mocks.tsx | 98 ++ src/problem3/original.tsx.txt | 81 + src/problem3/problem3.md | 99 ++ src/problem3/style.css | 292 ++++ src/problem3/view.tsx | 154 ++ src/problem4/.keep | 0 src/problem5/.keep | 0 src/shell.css | 294 ++++ tsconfig.json | 23 + vite.config.ts | 21 + 42 files changed, 6459 insertions(+), 38 deletions(-) create mode 100644 .gitignore create mode 100644 assets/solution2-preview.png create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/App.tsx create mode 100644 src/main.tsx delete mode 100644 src/problem1/.keep create mode 100644 src/problem1/lib/bigNumber.ts create mode 100644 src/problem1/lib/sumToN.ts create mode 100644 src/problem1/problem1.md create mode 100644 src/problem1/solutions.ts create mode 100644 src/problem1/style.css create mode 100644 src/problem1/tests/sumToN.test.ts create mode 100644 src/problem1/view.tsx create mode 100644 src/problem2/api.ts create mode 100644 src/problem2/balances.ts create mode 100644 src/problem2/components/TokenIcon.tsx create mode 100644 src/problem2/components/TokenSelect.tsx create mode 100644 src/problem2/data/balances.json create mode 100644 src/problem2/format.ts delete mode 100644 src/problem2/index.html create mode 100644 src/problem2/problem2.md delete mode 100644 src/problem2/script.js create mode 100644 src/problem2/tokenIcon.ts create mode 100644 src/problem2/types.ts create mode 100644 src/problem2/view.tsx delete mode 100644 src/problem3/.keep create mode 100644 src/problem3/Refactored.tsx create mode 100644 src/problem3/analysis.ts create mode 100644 src/problem3/mocks.tsx create mode 100644 src/problem3/original.tsx.txt create mode 100644 src/problem3/problem3.md create mode 100644 src/problem3/style.css create mode 100644 src/problem3/view.tsx delete mode 100644 src/problem4/.keep delete mode 100644 src/problem5/.keep create mode 100644 src/shell.css create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..ae582e1a55 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +.vite/ +*.log +.DS_Store +.env +.env.local diff --git a/assets/solution2-preview.png b/assets/solution2-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..110b42524b00c7b12c1c238e16697fd96e01e0b3 GIT binary patch literal 47969 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 literal 0 HcmV?d00001 diff --git a/index.html b/index.html new file mode 100644 index 0000000000..9b47b3c70a --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + 99Tech Code Challenge #1 + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000..4b8731674f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2135 @@ +{ + "name": "99tech-code-challenge", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "99tech-code-challenge", + "version": "1.0.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.6.3", + "vite": "^5.4.11", + "vitest": "^2.1.5" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.29", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.29.tgz", + "integrity": "sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", + "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.364", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz", + "integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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/node-releases": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", + "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "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/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "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/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000..ece7f94334 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "99tech-code-challenge", + "version": "1.0.0", + "private": true, + "type": "module", + "description": "99Tech Code Challenge #1 β€” three problems unified in a single tabbed React UI.", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.6.3", + "vite": "^5.4.11", + "vitest": "^2.1.5" + } +} diff --git a/readme.md b/readme.md index 1ff4bc95b4..1a24faf8af 100644 --- a/readme.md +++ b/readme.md @@ -1,10 +1,441 @@ -# 99Tech Code Challenge #1 # +# 99Tech Code Challenge #1 -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. +Solutions to the three problems, bundled as one React + Vite + TypeScript +app with a tab per problem. -It is important that you minimally attempt the problems, even if you do not arrive at a working solution. +--- -## 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. +## Problem 1 β€” Sum to N + +Four implementations of `sum_to_n(n)`. The first three obey the original +constraint (result fits in `Number.MAX_SAFE_INTEGER`); the fourth lifts +that constraint by doing every arithmetic step on decimal strings, so it +handles inputs whose answer has hundreds of digits. + +| Function | Approach | Time | Space | Range of `n` | +| ------------ | ------------------------------ | ------ | ----- | ----------------------------- | +| `sum_to_n_a` | Iterative loop | O(n) | O(1) | `\|n\|` ≀ 134,217,727 | +| `sum_to_n_b` | Gauss formula `nΒ·(n+1)/2` | O(1) | O(1) | `\|n\|` ≀ 134,217,727 | +| `sum_to_n_c` | `Array.from` + `reduce` | O(n) | O(n) | `\|n\|` ≀ 1,000,000 | +| `sum_to_n_d` | Big-number formula on strings | O(dΒ²) | O(d) | unbounded (d = digits of `n`) | + +Negative `n` returns the negation of the positive sum: +`sum_to_n(-5) === -15`. + +### Why the bounds + +- `a`, `b`, `c` need `nΒ·(n+1)/2 ≀ 2^53 βˆ’ 1`, which solves to + `n ≀ 134_217_727`. The UI rejects values above that with an explicit + error before invoking the function, so nothing overflows silently and + the browser tab never freezes. +- `c` is additionally bounded by array memory: `Array.from({ length: n })` + allocates `n` slots, so the practical cap is ~1M elements. +- `d` is bounded only by available memory and patience β€” the helpers are + O(dΒ²), and a 1000-digit `n` still finishes in well under a second. + +### sum_to_n_a β€” iterative loop + +```js +var sum_to_n_a = function (n) { + if (n < 0) return -sum_to_n_a(-n); + let sum = 0; + for (let i = 1; i <= n; i++) sum += i; + return sum; +}; +``` + +The direct reading of "add every number from 1 to n". `n` additions, +constant memory. + +### sum_to_n_b β€” Gauss formula + +```js +var sum_to_n_b = function (n) { + if (n < 0) return -sum_to_n_b(-n); + return (n * (n + 1)) / 2; +}; +``` + +Pair `1 + n`, `2 + (nβˆ’1)`, … each pair sums to `n + 1` and there are +`n / 2` pairs, so the total is `n Β· (n + 1) / 2`. Constant time, no +loop. The right answer whenever the result fits in +`Number.MAX_SAFE_INTEGER`. + +### sum_to_n_c β€” functional reduce + +```js +var sum_to_n_c = function (n) { + if (n < 0) return -sum_to_n_c(-n); + return Array.from({ length: n }, (_, i) => i + 1) + .reduce((a, b) => a + b, 0); +}; +``` + +Builds `[1, 2, …, n]` and folds it with `reduce`. Same number of +additions as the loop, but allocates an array of length `n` β€” included +as a "functional style" contrast, not because it is faster. + +### sum_to_n_d β€” big-number Gauss + +Same identity as `sum_to_n_b`, but every arithmetic step is performed on +decimal strings so the answer is not bounded by `Number.MAX_SAFE_INTEGER`: + +1. Parse the input string, peel off the sign. +2. `addBigNumbers(n, "1")` β€” schoolbook addition right-to-left, carry + into the next column. +3. `mulBigNumbers(n, n+1)` β€” long multiplication, `O(dΒ²)` where `d` is + the digit count. +4. `divBy2(product)` β€” single-digit long division by 2. The product is + always even (`n` and `n+1` are consecutive, so exactly one is even), + so the floor division is exact. +5. Reattach the sign. + +```js +sum_to_n_d('123456789012345678901234567890') +// β†’ '7620789614188397919306039269735013345025385542661216720495' +``` + +The string-addition core (from the prompt) is: + +```js +function addBigNumbers(a, b) { + let i = a.length - 1, j = b.length - 1, carry = 0, result = ''; + while (i >= 0 || j >= 0 || carry > 0) { + let sum = carry; + if (i >= 0) sum += a.charCodeAt(i--) - 48; + if (j >= 0) sum += b.charCodeAt(j--) - 48; + carry = (sum / 10) | 0; + result = (sum % 10) + result; + } + return result; +} +``` + +The signed version in [`lib/bigNumber.ts`](src/problem1/lib/bigNumber.ts) +reduces mixed-sign cases to a magnitude subtraction via a separate +`subAbs` helper, but the additive core is exactly the snippet above. + +### Tests + +`npm test` runs 294 vitest cases: +[`src/problem1/tests/sumToN.test.ts`](src/problem1/tests/sumToN.test.ts). +The big-number variant is verified against `BigInt` for a 30-digit input. + +--- + +## Problem 2 β€” Fancy Currency Swap + +A currency swap form backed by the live +[Switcheo price feed](https://interview.switcheo.com/prices.json) and a +mock wallet that gets actually debited / credited per swap. + +![Swap form preview](assets/solution2-preview.png) + +### Pipeline + +``` +prices.json ──► fetchPrices ──► dedupeLatest ──► meta[] + β”‚ +balances.json ──► loadInitialBalances ──► balances ── + β–Ό + tokens = meta β‹ˆ balances + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ SWAP FORM β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + onSubmit ───── + β–Ό + applySwap(balances, ...) + β”‚ + setBalances(new) +``` + +- `meta` is the static per-token info (symbol, price, icon URL) computed + once after fetch. +- `balances` is a mutable `Record` seeded from + [`data/balances.json`](src/problem2/data/balances.json) and updated + immutably by `applySwap` on every successful submit. The FROM token is + debited, the TO token credited, so subsequent swaps see the new + numbers everywhere (dropdown, `MAX` button, "Insufficient balance" + validation). +- `tokens` is a memo joining the two so React re-renders the UI whenever + either input changes. + +### Quote math + +```ts +rate = priceFrom / priceTo +grossOut = amount * rate +feeAmount = grossOut * 0.003 // mock 0.30% protocol fee +amountOut = grossOut - feeAmount +minReceived = amountOut * (1 - slippage / 100) +``` + +`slippage` is the user-selectable tolerance (0.1% / 0.5% / 1% / custom) +shown under the form. `minReceived` is what a real AMM would enforce +on-chain to prevent the user from receiving worse-than-expected output +if the pool ratio shifts before the tx lands. + +### Validation + +- Both tokens required. +- FROM and TO must be different. +- Amount must parse as a positive number. +- Amount must not exceed `balances[FROM]`. + +All four checks short-circuit the submit button and surface a single +inline message under the summary. + +### Loading + submit + +Token prices are fetched on first render; an error state offers a +single-button retry. On submit the form goes into a `submitting` state +for 1.5s (per the problem's "mock backend with a loading indicator" +hint), then commits the balance update and shows a success toast that +auto-dismisses. + +### Files of interest + +- [`view.tsx`](src/problem2/view.tsx) β€” the form, all state, the + quote/validation memos +- [`api.ts`](src/problem2/api.ts) β€” fetch + dedupe to latest price per + currency +- [`balances.ts`](src/problem2/balances.ts) β€” JSON seed, hash fallback + for unlisted symbols, `applySwap` immutable update +- [`components/TokenSelect.tsx`](src/problem2/components/TokenSelect.tsx) + β€” searchable dropdown with icon + balance + USD price +- [`components/TokenIcon.tsx`](src/problem2/components/TokenIcon.tsx) β€” + Switcheo SVG icon with a coloured letter-avatar fallback + +--- + +## Problem 3 β€” Messy React + +The prompt asks for **(a)** a list of inefficiencies / anti-patterns in +the supplied `WalletPage` and **(b)** a refactored version. More points +go to the analysis, so the catalogue is primary and the refactor is +secondary. + +### Issues catalogued + +17 issues across three severities, surfaced in the UI as filterable +cards. Highlights: + +| # | Severity | Issue | +| - | ------------ | ---------------------------------------------------------------- | +| 1 | bug | Filter references undefined variable `lhsPriority` | +| 2 | bug | Filter logic inverted β€” keeps balances with `amount <= 0` | +| 3 | bug | `formattedBalances` computed but never consumed by `rows` | +| 4 | bug | Sort comparator returns `undefined` for equal priorities | +| 5 | bug | `WalletBalance` is missing the `blockchain` field | +| 6 | anti-pattern | `getPriority(blockchain: any)` defeats the type system | +| 7 | perf | `getPriority` re-declared every render | +| 8 | perf | Sort runs `getPriority` O(n log n) times | +| 9 | perf | `useMemo` depends on `prices` but doesn't use it | +| 10 | perf | `formattedBalances` not memoised | +| 11 | anti-pattern | `key={index}` for a sorted list | +| 12 | bug | `prices[balance.currency]` may be undefined β†’ `NaN` in the UI | +| 13 | anti-pattern | Empty `interface Props extends BoxProps {}` | +| 14 | anti-pattern | `children` destructured but never rendered | +| 15 | bug | `toFixed()` without precision argument | +| 16 | bug | `rows.map` types items as `FormattedWalletBalance` | +| 17 | perf | `usdValue` recomputed inside the render path | + +Full text for each issue lives in +[`src/problem3/analysis.ts`](src/problem3/analysis.ts) and is rendered +verbatim in the Problem 3 tab. + +### Original code, annotated + +Same numbering as the table above β€” every `// #N` comment lines up with +the row of matching number. + +```tsx +interface WalletBalance { + currency: string; + amount: number; + // #5 bug: missing `blockchain` field β€” the component reads + // `balance.blockchain` below, so the type is a lie. +} + +interface FormattedWalletBalance { + currency: string; + amount: number; + formatted: string; +} + +interface Props extends BoxProps { + // #13 anti-pattern: empty interface, redundant alias for BoxProps. +} + +const WalletPage: React.FC = (props: Props) => { + const { children, ...rest } = props; + // #14 anti-pattern: `children` destructured but never rendered β€” any + // JSX a parent passes silently disappears. + + const balances = useWalletBalances(); + const prices = usePrices(); + + const getPriority = (blockchain: any): number => { + // #6 anti-pattern: `any` defeats the type system β€” should be a + // `Blockchain` union. + // #7 perf: function re-declared on every render β€” pure and + // closure-free, hoist it to module scope. + switch (blockchain) { + case 'Osmosis': return 100; + case 'Ethereum': return 50; + case 'Arbitrum': return 30; + case 'Zilliqa': return 20; + case 'Neo': return 20; + default: return -99; + } + }; + + const sortedBalances = useMemo(() => { + return balances + .filter((balance: WalletBalance) => { + const balancePriority = getPriority(balance.blockchain); + if (lhsPriority > -99) { + // #1 bug: `lhsPriority` is not defined in scope β€” should be + // `balancePriority`. ReferenceError in strict mode, + // silently `undefined > -99 === false` otherwise. + if (balance.amount <= 0) { + // #2 bug: keeps balances with amount <= 0 β€” inverted; the + // intent is `balance.amount > 0`. + return true; + } + } + return false; + }) + .sort((lhs: WalletBalance, rhs: WalletBalance) => { + const leftPriority = getPriority(lhs.blockchain); + const rightPriority = getPriority(rhs.blockchain); + // #8 perf: getPriority runs twice per comparison β†’ O(n log n) + // calls total. Precompute once per row via .map first. + if (leftPriority > rightPriority) return -1; + else if (rightPriority > leftPriority) return 1; + // #4 bug: no `return 0` for equal priorities β€” comparator + // implicitly returns `undefined`, violating the sort + // contract. + }); + }, [balances, prices]); + // #9 perf: `prices` is in the dependency array but the memoised + // callback never reads it β€” recomputes on every price tick. + + const formattedBalances = sortedBalances.map((balance: WalletBalance) => { + // #10 perf: not memoised β€” re-runs every render. + return { + ...balance, + formatted: balance.amount.toFixed(), + // #15 bug: `toFixed()` defaults to 0 decimals β†’ "1.5" becomes "2". + }; + }); + // #3 bug: `formattedBalances` is built but never used β€” `rows` below + // maps `sortedBalances` instead, so `balance.formatted` is + // `undefined` at render time. + + const rows = sortedBalances.map( + (balance: FormattedWalletBalance, index: number) => { + // #16 bug: items are typed as FormattedWalletBalance but the + // array is WalletBalance[] β€” the cast hides #3. + const usdValue = prices[balance.currency] * balance.amount; + // #12 bug: `prices[currency]` may be undefined β†’ `NaN` in UI. + // #17 perf: usdValue is recomputed in the render path on every + // pass β€” belongs in the memoised pipeline. + return ( + + ); + }, + ); + + return
{rows}
; +}; +``` + +### What the refactor does + +[`Refactored.tsx`](src/problem3/Refactored.tsx) addresses every issue +above: + +- `Blockchain` union type and `WalletBalance` extended with + `blockchain: Blockchain`. +- `getPriority` hoisted to module scope, backed by a `Record` lookup. +- Single `useMemo` that filters β†’ maps to `(balance, priority, price)` + β†’ sorts by precomputed priority (Schwartzian transform, so the + lookup is O(n) instead of O(n log n)) β†’ formats with `toFixed(4)` and + computes `usdValue`. +- `key={balance.currency}` instead of `key={index}`. +- Null-safe `prices[currency] ?? 0`. +- The empty `Props` interface and the unused `children` destructure + are dropped; the component just spreads `...props` so the wrapping + `
` stays transparent. + +The refactored component actually mounts in the Problem 3 tab, with +`useWalletBalances`, `usePrices`, `WalletRow`, and `BoxProps` provided +by [`mocks.tsx`](src/problem3/mocks.tsx). + +--- + +## Demo / running the project + +```bash +npm install +npm run dev # interactive UI at http://localhost:5173 +npm test # vitest (294 tests, problem 1) +npm run typecheck # tsc --noEmit +npm run build # static build into ./dist +``` + +Tabs use hash routing β€” go straight to a problem via +`#/problem1`, `#/problem2`, or `#/problem3`. + +### Stack + +- Vite 5 + React 18 + TypeScript 5 (strict mode) +- Vitest for the Problem 1 algorithm tests +- No runtime deps beyond React + ReactDOM + +### Layout + +``` +code-challenge/ +β”œβ”€β”€ package.json, tsconfig.json, vite.config.ts, index.html +β”œβ”€β”€ assets/ +β”‚ └── solution2-preview.png +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ main.tsx, App.tsx, shell.css # tab shell +β”‚ β”œβ”€β”€ problem1/ +β”‚ β”‚ β”œβ”€β”€ lib/{sumToN,bigNumber}.ts # 4 implementations + helpers +β”‚ β”‚ β”œβ”€β”€ tests/sumToN.test.ts # vitest, 294 tests +β”‚ β”‚ β”œβ”€β”€ solutions.ts # algorithm metadata +β”‚ β”‚ β”œβ”€β”€ view.tsx, style.css +β”‚ β”œβ”€β”€ problem2/ +β”‚ β”‚ β”œβ”€β”€ view.tsx, style.css +β”‚ β”‚ β”œβ”€β”€ api.ts, types.ts # fetch + dedupe prices +β”‚ β”‚ β”œβ”€β”€ balances.ts # JSON seed + applySwap +β”‚ β”‚ β”œβ”€β”€ data/balances.json # initial wallet snapshot +β”‚ β”‚ β”œβ”€β”€ format.ts, tokenIcon.ts +β”‚ β”‚ └── components/{TokenSelect,TokenIcon}.tsx +β”‚ └── problem3/ +β”‚ β”œβ”€β”€ analysis.ts # 17 catalogued issues +β”‚ β”œβ”€β”€ Refactored.tsx # working WalletPage +β”‚ β”œβ”€β”€ mocks.tsx # useWalletBalances / usePrices / ... +β”‚ β”œβ”€β”€ original.tsx.txt # prompt code, raw-imported +β”‚ β”œβ”€β”€ view.tsx, style.css +``` + +### Submission + +Either link to this repository or attach the build output β€” every problem +has its source under `src/` and is reachable from the tabbed UI started +with `npm run dev`. diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000000..a84a2b20c8 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,80 @@ +import { useEffect, useState } from 'react'; +import { Problem1View } from './problem1/view'; +import { Problem2View } from './problem2/view'; +import { Problem3View } from './problem3/view'; + +type TabId = 'problem1' | 'problem2' | 'problem3'; + +const TABS: { id: TabId; label: string; blurb: string }[] = [ + { + id: 'problem1', + label: 'Problem 1 β€” Sum to N', + blurb: 'Four implementations: iterative, formula, functional, big-number.', + }, + { + id: 'problem2', + label: 'Problem 2 β€” Fancy Swap', + blurb: 'Currency swap form with live Switcheo prices.', + }, + { + id: 'problem3', + label: 'Problem 3 β€” Messy React', + blurb: 'Issue analysis + refactored WalletPage live demo.', + }, +]; + +const parseHash = (): TabId => { + const raw = window.location.hash.replace(/^#\/?/, ''); + if (raw === 'problem2' || raw === 'problem3') return raw; + return 'problem1'; +}; + +export function App() { + const [tab, setTab] = useState(parseHash); + + useEffect(() => { + const onHashChange = () => setTab(parseHash()); + window.addEventListener('hashchange', onHashChange); + return () => window.removeEventListener('hashchange', onHashChange); + }, []); + + const activeTab = TABS.find((t) => t.id === tab)!; + + return ( +
+
+
+ 99Tech +

Code Challenge #1

+
+ +

{activeTab.blurb}

+
+ +
+ {tab === 'problem1' && } + {tab === 'problem2' && } + {tab === 'problem3' && } +
+ +
+ + Source on GitHub Β· Run with npm run dev Β· Tests with{' '} + npm test + +
+
+ ); +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000000..fba284fe94 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import './shell.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/src/problem1/.keep b/src/problem1/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/problem1/lib/bigNumber.ts b/src/problem1/lib/bigNumber.ts new file mode 100644 index 0000000000..7a5c730ae1 --- /dev/null +++ b/src/problem1/lib/bigNumber.ts @@ -0,0 +1,129 @@ +// Decimal big-number arithmetic on plain strings. +// +// Inputs are integer strings, optionally prefixed with '-'. +// All exported operations preserve sign correctly and never lose precision. +// Used by sum_to_n_d to compute n * (n + 1) / 2 without hitting +// Number.MAX_SAFE_INTEGER. + +export const stripLeadingZeros = (s: string): string => { + let i = 0; + while (i < s.length - 1 && s.charCodeAt(i) === 48) i++; + return s.slice(i); +}; + +// Compare magnitudes of two non-negative decimal strings. Returns -1, 0, or 1. +export const cmpAbs = (a: string, b: string): -1 | 0 | 1 => { + const aa = stripLeadingZeros(a); + const bb = stripLeadingZeros(b); + if (aa.length !== bb.length) return aa.length < bb.length ? -1 : 1; + if (aa < bb) return -1; + if (aa > bb) return 1; + return 0; +}; + +interface Split { + sign: 1 | -1; + abs: string; +} + +const splitSign = (s: string): Split => { + if (s.startsWith('-')) return { sign: -1, abs: s.slice(1) }; + return { sign: 1, abs: s }; +}; + +const applySign = (sign: 1 | -1, abs: string): string => { + const stripped = stripLeadingZeros(abs); + if (stripped === '0') return '0'; + return sign === -1 ? '-' + stripped : stripped; +}; + +// Add two non-negative decimal strings, right-to-left, with carry. +const addAbs = (a: string, b: string): string => { + let i = a.length - 1; + let j = b.length - 1; + let carry = 0; + let result = ''; + while (i >= 0 || j >= 0 || carry > 0) { + let sum = carry; + if (i >= 0) sum += a.charCodeAt(i--) - 48; + if (j >= 0) sum += b.charCodeAt(j--) - 48; + carry = (sum / 10) | 0; + result = (sum % 10) + result; + } + return result; +}; + +// Subtract b from a assuming a >= b >= 0 (both non-negative decimal strings). +const subAbs = (a: string, b: string): string => { + let i = a.length - 1; + let j = b.length - 1; + let borrow = 0; + let result = ''; + while (i >= 0) { + let diff = (a.charCodeAt(i--) - 48) - borrow; + if (j >= 0) diff -= (b.charCodeAt(j--) - 48); + if (diff < 0) { + diff += 10; + borrow = 1; + } else { + borrow = 0; + } + result = diff + result; + } + return stripLeadingZeros(result); +}; + +// Signed addition. Handles all four sign combinations by reducing to +// addAbs / subAbs of magnitudes. +export const addBigNumbers = (a: string, b: string): string => { + const A = splitSign(a); + const B = splitSign(b); + if (A.sign === B.sign) { + return applySign(A.sign, addAbs(A.abs, B.abs)); + } + const cmp = cmpAbs(A.abs, B.abs); + if (cmp === 0) return '0'; + if (cmp > 0) return applySign(A.sign, subAbs(A.abs, B.abs)); + return applySign(B.sign, subAbs(B.abs, A.abs)); +}; + +// Long multiplication, O(d_a * d_b). +const mulAbs = (a: string, b: string): string => { + const A = stripLeadingZeros(a); + const B = stripLeadingZeros(b); + if (A === '0' || B === '0') return '0'; + const out: number[] = new Array(A.length + B.length).fill(0); + for (let i = A.length - 1; i >= 0; i--) { + const da = A.charCodeAt(i) - 48; + for (let j = B.length - 1; j >= 0; j--) { + const db = B.charCodeAt(j) - 48; + const pos = i + j + 1; + const total = out[pos] + da * db; + out[pos] = total % 10; + out[pos - 1] += (total / 10) | 0; + } + } + return stripLeadingZeros(out.join('')); +}; + +export const mulBigNumbers = (a: string, b: string): string => { + const A = splitSign(a); + const B = splitSign(b); + const sign = (A.sign * B.sign) as 1 | -1; + return applySign(sign, mulAbs(A.abs, B.abs)); +}; + +// Floor division of a decimal string by 2, single-digit long division. +// The only caller (sum_to_n_d) always divides a non-negative even product, +// so the floor matches the true quotient. +export const divBy2 = (s: string): string => { + const { sign, abs } = splitSign(s); + let result = ''; + let carry = 0; + for (let i = 0; i < abs.length; i++) { + const cur = carry * 10 + (abs.charCodeAt(i) - 48); + result += ((cur / 2) | 0).toString(); + carry = cur % 2; + } + return applySign(sign, result); +}; diff --git a/src/problem1/lib/sumToN.ts b/src/problem1/lib/sumToN.ts new file mode 100644 index 0000000000..8f4faeff28 --- /dev/null +++ b/src/problem1/lib/sumToN.ts @@ -0,0 +1,54 @@ +// Four implementations of sum_to_n. +// +// sum_to_n_a β€” iterative loop O(n) time, O(1) space +// sum_to_n_b β€” Gauss closed-form formula O(1) time, O(1) space +// sum_to_n_c β€” Array.from + reduce O(n) time, O(n) space +// sum_to_n_d β€” big-number Gauss on strings O(d^2) where d = digits of n +// +// a / b / c assume the result fits in Number.MAX_SAFE_INTEGER, per the +// problem statement. d lifts that restriction by doing every arithmetic +// step on decimal strings; it accepts number | string and returns string. +// +// For negative n, every variant returns the negation of the positive sum: +// sum_to_n(-5) === -(1 + 2 + 3 + 4 + 5) === -15. + +import { addBigNumbers, mulBigNumbers, divBy2 } from './bigNumber'; + +export const sum_to_n_a = (n: number): number => { + if (n < 0) return -sum_to_n_a(-n); + let sum = 0; + for (let i = 1; i <= n; i++) sum += i; + return sum; +}; + +export const sum_to_n_b = (n: number): number => { + if (n < 0) return -sum_to_n_b(-n); + return (n * (n + 1)) / 2; +}; + +export const sum_to_n_c = (n: number): number => { + if (n < 0) return -sum_to_n_c(-n); + return Array.from({ length: n }, (_, i) => i + 1).reduce((a, b) => a + b, 0); +}; + +// Accepts number | string. Always returns a string, because the result can +// exceed Number.MAX_SAFE_INTEGER. +export const sum_to_n_d = (n: number | string): string => { + const input = String(n).trim(); + if (!/^-?\d+$/.test(input)) { + throw new Error( + `sum_to_n_d: invalid integer input: ${JSON.stringify(input)}`, + ); + } + + const negative = input.startsWith('-'); + const abs = negative ? input.slice(1) : input; + + // sum = abs * (abs + 1) / 2 + const absPlus1 = addBigNumbers(abs, '1'); + const product = mulBigNumbers(abs, absPlus1); + const half = divBy2(product); + + if (half === '0') return '0'; + return negative ? '-' + half : half; +}; diff --git a/src/problem1/problem1.md b/src/problem1/problem1.md new file mode 100644 index 0000000000..6b8ac424ee --- /dev/null +++ b/src/problem1/problem1.md @@ -0,0 +1,31 @@ +# Problem 1: Three ways to sum to n + + + +# Task + +Provide 3 unique implementations of the following function in JavaScript. + +**Input**: `n` - any integer + +*Assuming this input will always produce a result lesser than `Number.MAX_SAFE_INTEGER`*. + +**Output**: `return` - summation to `n`, i.e. `sum_to_n(5) === 1 + 2 + 3 + 4 + 5 === 15`. + +```jsx +var sum_to_n_a = function(n) { + // your code here +}; + +var sum_to_n_b = function(n) { + // your code here +}; + +var sum_to_n_c = function(n) { + // your code here +}; +``` \ No newline at end of file diff --git a/src/problem1/solutions.ts b/src/problem1/solutions.ts new file mode 100644 index 0000000000..fcb9fefdce --- /dev/null +++ b/src/problem1/solutions.ts @@ -0,0 +1,146 @@ +// Metadata + display text for the four sum_to_n solutions. Rendered in +// the left-hand panel of the Problem 1 tab when a solution is selected. + +export type SolutionId = 'a' | 'b' | 'c' | 'd'; + +export interface Solution { + id: SolutionId; + name: string; + time: string; + space: string; + explanation: string; + code: string; +} + +export const solutions: Record = { + a: { + id: 'a', + name: 'sum_to_n_a β€” Iterative Loop', + time: 'O(n)', + space: 'O(1)', + explanation: + 'Walks from 1 up to n with a counter, accumulating the running total ' + + 'in a single variable. This is the most direct translation of "add ' + + 'every number from 1 to n". It performs n additions, so runtime grows ' + + 'linearly with n; for n in the millions it is still fast (sub-second) ' + + 'but cannot beat the constant-time formula. Negative n is handled by ' + + 'recursing on -n and negating the result, so sum_to_n_a(-5) = ' + + '-(1 + 2 + 3 + 4 + 5) = -15.', + code: `var sum_to_n_a = function (n) { + if (n < 0) return -sum_to_n_a(-n); + let sum = 0; + for (let i = 1; i <= n; i++) sum += i; + return sum; +};`, + }, + + b: { + id: 'b', + name: 'sum_to_n_b β€” Gauss Formula', + time: 'O(1)', + space: 'O(1)', + explanation: + 'Uses the closed-form arithmetic-series formula n * (n + 1) / 2. ' + + 'Pair 1 + n, 2 + (nβˆ’1), ... β€” every pair sums to n + 1 and there are ' + + 'n / 2 pairs, so the total is n Β· (n + 1) / 2. Constant time and ' + + 'constant space regardless of n, so this is the right choice whenever ' + + 'the result fits in Number.MAX_SAFE_INTEGER.', + code: `var sum_to_n_b = function (n) { + if (n < 0) return -sum_to_n_b(-n); + return (n * (n + 1)) / 2; +};`, + }, + + c: { + id: 'c', + name: 'sum_to_n_c β€” Functional (Array.from + reduce)', + time: 'O(n)', + space: 'O(n)', + explanation: + 'Builds the array [1, 2, ..., n] with Array.from and folds it into a ' + + 'single sum with reduce. Same number of additions as the iterative ' + + 'loop, but it also allocates an array of length n, so it uses O(n) ' + + 'memory. Included as a "functional style" contrast to the imperative ' + + 'loop, not because it is faster. For very large n the allocation ' + + 'fails before the sum does.', + code: `var sum_to_n_c = function (n) { + if (n < 0) return -sum_to_n_c(-n); + return Array.from({ length: n }, (_, i) => i + 1) + .reduce((a, b) => a + b, 0); +};`, + }, + + d: { + id: 'd', + name: 'sum_to_n_d β€” Big-Number Formula (decimal strings)', + time: 'O(dΒ²) where d = digits of n', + space: 'O(d)', + explanation: + 'Same Gauss identity as sum_to_n_b, but every arithmetic step is ' + + 'performed on decimal strings so the result is not bounded by ' + + 'Number.MAX_SAFE_INTEGER. Pipeline: (1) parse the input string and ' + + 'peel off the sign; (2) addBigNumbers(n, "1") computes n + 1 by ' + + 'schoolbook addition right-to-left with a carry; (3) ' + + 'mulBigNumbers(n, n + 1) does long multiplication, O(dΒ²); (4) ' + + 'divBy2(product) does single-digit long division by 2 β€” the product ' + + 'is always even because n and n+1 are consecutive, so the result is ' + + 'exact; (5) reattach the sign. Accepts number | string and always ' + + 'returns a string, e.g. sum_to_n_d("123456789012345678901234567890") ' + + 'yields a 58-digit answer.', + code: `// Accepts number | string. Returns string. +var sum_to_n_d = function (n) { + const input = String(n).trim(); + if (!/^-?\\d+$/.test(input)) throw new Error('Invalid integer'); + + const negative = input.startsWith('-'); + const abs = negative ? input.slice(1) : input; + + const absPlus1 = addBigNumbers(abs, '1'); + const product = mulBigNumbers(abs, absPlus1); + const half = divBy2(product); + + if (half === '0') return '0'; + return negative ? '-' + half : half; +}; + +// --- string-based helpers (non-negative inputs) --- + +function addBigNumbers(a, b) { + let i = a.length - 1, j = b.length - 1, carry = 0, result = ''; + while (i >= 0 || j >= 0 || carry > 0) { + let sum = carry; + if (i >= 0) sum += a.charCodeAt(i--) - 48; + if (j >= 0) sum += b.charCodeAt(j--) - 48; + carry = (sum / 10) | 0; + result = (sum % 10) + result; + } + return result; +} + +function mulBigNumbers(a, b) { + if (a === '0' || b === '0') return '0'; + const out = new Array(a.length + b.length).fill(0); + for (let i = a.length - 1; i >= 0; i--) { + const da = a.charCodeAt(i) - 48; + for (let j = b.length - 1; j >= 0; j--) { + const db = b.charCodeAt(j) - 48; + const pos = i + j + 1; + const total = out[pos] + da * db; + out[pos] = total % 10; + out[pos - 1] += (total / 10) | 0; + } + } + return out.join('').replace(/^0+/, '') || '0'; +} + +function divBy2(s) { + let result = '', carry = 0; + for (let i = 0; i < s.length; i++) { + const cur = carry * 10 + (s.charCodeAt(i) - 48); + result += ((cur / 2) | 0).toString(); + carry = cur % 2; + } + return result.replace(/^0+/, '') || '0'; +}`, + }, +}; diff --git a/src/problem1/style.css b/src/problem1/style.css new file mode 100644 index 0000000000..ebef9b3197 --- /dev/null +++ b/src/problem1/style.css @@ -0,0 +1,283 @@ +/* Problem 1 β€” Sum to N */ + +.p1.layout-split { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 20px; +} + +@media (max-width: 1080px) { + .p1.layout-split { + grid-template-columns: 1fr; + } +} + +/* ---------------- solution nav ---------------- */ + +.p1-sol-nav { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.p1-sol-btn { + background: var(--panel-2); + border: 1px solid var(--border); + color: var(--text); + padding: 10px 12px; + border-radius: var(--radius-sm); + cursor: pointer; + text-align: left; + font-family: inherit; + transition: all 0.1s ease; + display: flex; + flex-direction: column; + gap: 2px; +} + +.p1-sol-btn:hover { + border-color: var(--accent); + color: var(--text-strong); +} + +.p1-sol-btn.active { + background: var(--accent-bg); + border-color: var(--accent); + color: var(--text-strong); +} + +.p1-sol-btn-id { + font-weight: 600; + font-size: 13px; +} + +.p1-sol-btn-tag { + font-size: 11px; + color: var(--muted); +} + +.p1-sol-btn.active .p1-sol-btn-tag { + color: var(--accent-strong); +} + +/* ---------------- solution detail ---------------- */ + +.p1-sol-detail h3 { + margin: 20px 0 10px; + font-size: 17px; + font-weight: 600; +} + +.p1-sol-meta { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.meta-pill { + background: var(--code-bg); + border: 1px solid var(--code-border); + padding: 4px 10px; + border-radius: 4px; + font-size: 12px; + color: var(--muted); + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; +} + +.meta-pill strong { + color: var(--text-strong); + font-weight: 600; + margin-left: 4px; +} + +.p1-sol-explanation { + font-size: 14px; + line-height: 1.65; + margin: 0; +} + +.p1-sol-code { + background: var(--code-bg); + border: 1px solid var(--code-border); + border-radius: var(--radius-sm); + padding: 14px 16px; + overflow: auto; + font-size: 12.5px; + line-height: 1.55; + margin: 0; + max-height: 520px; +} + +/* ---------------- input + result ---------------- */ + +.p1-input-row { + margin-bottom: 10px; +} + +.p1-input { + width: 100%; + background: var(--code-bg); + border: 1px solid var(--border); + color: var(--text-strong); + padding: 10px 12px; + border-radius: var(--radius-sm); + font-size: 14px; +} + +.p1-input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 2px var(--accent-bg); +} + +.p1-button-row { + display: flex; + gap: 8px; +} + +.p1-input-hint { + font-size: 11.5px; + color: var(--muted); + margin: 6px 2px 10px; +} + +.p1-input-hint code { + background: var(--code-bg); + border: 1px solid var(--code-border); + padding: 0 4px; + border-radius: 3px; + font-size: 11px; + color: var(--text); +} + +.p1-input-hint strong { + color: var(--text-strong); + font-weight: 600; +} + +.p1-result-warning { + margin-top: 8px; + padding: 6px 10px; + background: rgba(240, 182, 87, 0.1); + border-left: 2px solid var(--warn); + border-radius: 0 4px 4px 0; + font-size: 11.5px; + color: var(--warn); +} + +.p1-result { + background: var(--code-bg); + border: 1px solid var(--code-border); + border-radius: var(--radius-sm); + padding: 14px 16px; + min-height: 64px; + font-size: 13px; + word-break: break-all; +} + +.p1-result.ok { + border-color: var(--pass); +} + +.p1-result.error { + border-color: var(--fail); +} + +.p1-result-placeholder { + color: var(--muted); + font-style: italic; +} + +.p1-result-value { + font-size: 15px; + font-weight: 600; + color: var(--text-strong); +} + +.p1-result.error .p1-result-value { + color: var(--fail); +} + +.p1-result-meta { + color: var(--muted); + font-size: 11.5px; + margin-top: 6px; +} + +/* ---------------- tests ---------------- */ + +.p1-test-summary { + font-size: 13px; + color: var(--muted); + margin-bottom: 8px; +} + +.p1-test-summary.all-pass { + color: var(--pass); + font-weight: 600; +} + +.p1-test-summary.some-fail { + color: var(--fail); + font-weight: 600; +} + +.p1-test-list { + list-style: none; + padding: 0; + margin: 0; + max-height: 320px; + overflow-y: auto; + border: 1px solid var(--border); + border-radius: var(--radius-sm); +} + +.p1-test-item { + display: grid; + grid-template-columns: 22px minmax(0, 1fr); + grid-template-rows: auto auto; + column-gap: 6px; + row-gap: 2px; + padding: 8px 12px; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 12px; + border-bottom: 1px solid var(--border); +} + +.p1-test-item:last-child { + border-bottom: none; +} + +.p1-test-item.pass .p1-test-mark { + color: var(--pass); + font-weight: 700; +} + +.p1-test-item.fail .p1-test-mark { + color: var(--fail); + font-weight: 700; +} + +.p1-test-mark { + grid-row: 1 / span 2; + font-size: 14px; +} + +.p1-test-label { + color: var(--text-strong); + font-weight: 600; +} + +.p1-test-expected { + color: var(--muted); + font-size: 11px; +} + +.p1-test-expected code { + background: var(--code-bg); + padding: 1px 5px; + border-radius: 3px; + border: 1px solid var(--code-border); + font-size: 11px; + color: var(--text); +} diff --git a/src/problem1/tests/sumToN.test.ts b/src/problem1/tests/sumToN.test.ts new file mode 100644 index 0000000000..b1f6ba67d1 --- /dev/null +++ b/src/problem1/tests/sumToN.test.ts @@ -0,0 +1,149 @@ +import { describe, it, expect } from 'vitest'; +import { + sum_to_n_a, + sum_to_n_b, + sum_to_n_c, + sum_to_n_d, +} from '../lib/sumToN'; +import { + addBigNumbers, + mulBigNumbers, + divBy2, + cmpAbs, +} from '../lib/bigNumber'; + +const BASIC_CASES: Array<[number, number]> = [ + [0, 0], + [1, 1], + [2, 3], + [5, 15], + [10, 55], + [100, 5050], + [1000, 500500], + [-1, -1], + [-5, -15], + [-100, -5050], +]; + +describe.each([ + ['a', sum_to_n_a], + ['b', sum_to_n_b], + ['c', sum_to_n_c], +] as const)('sum_to_n_%s', (label, fn) => { + for (const [n, expected] of BASIC_CASES) { + it(`sum_to_n_${label}(${n}) === ${expected}`, () => { + expect(fn(n)).toBe(expected); + }); + } +}); + +describe('a / b / c agree on the integer range', () => { + for (let n = -50; n <= 200; n++) { + it(`agree at n=${n}`, () => { + const a = sum_to_n_a(n); + const b = sum_to_n_b(n); + const c = sum_to_n_c(n); + expect(a).toBe(b); + expect(b).toBe(c); + }); + } +}); + +describe('sum_to_n_d', () => { + it('accepts number input', () => { + expect(sum_to_n_d(0)).toBe('0'); + expect(sum_to_n_d(5)).toBe('15'); + expect(sum_to_n_d(100)).toBe('5050'); + }); + + it('accepts string input', () => { + expect(sum_to_n_d('0')).toBe('0'); + expect(sum_to_n_d('5')).toBe('15'); + expect(sum_to_n_d('1000000')).toBe('500000500000'); + }); + + it('handles inputs whose result exceeds MAX_SAFE_INTEGER', () => { + expect(sum_to_n_d('100000000000')).toBe('5000000000050000000000'); + }); + + it('handles a 30-digit input (verified against BigInt)', () => { + const n = '123456789012345678901234567890'; + const big = BigInt(n); + const expected = ((big * (big + 1n)) / 2n).toString(); + expect(sum_to_n_d(n)).toBe(expected); + }); + + it('handles negative input', () => { + expect(sum_to_n_d(-5)).toBe('-15'); + expect(sum_to_n_d('-100')).toBe('-5050'); + expect(sum_to_n_d('-100000000000')).toBe('-5000000000050000000000'); + }); + + it('matches a / b / c on the small integer range', () => { + for (let n = -30; n <= 30; n++) { + expect(sum_to_n_d(n)).toBe(String(sum_to_n_a(n))); + } + }); + + it('throws on invalid input', () => { + expect(() => sum_to_n_d('abc')).toThrow(); + expect(() => sum_to_n_d('1.5')).toThrow(); + expect(() => sum_to_n_d('')).toThrow(); + expect(() => sum_to_n_d('--5')).toThrow(); + }); +}); + +describe('bigNumber helpers', () => { + describe('addBigNumbers', () => { + it('adds positive numbers', () => { + expect(addBigNumbers('0', '0')).toBe('0'); + expect(addBigNumbers('999', '1')).toBe('1000'); + expect(addBigNumbers('123', '456')).toBe('579'); + expect(addBigNumbers('99999999999999999999', '1')).toBe( + '100000000000000000000', + ); + }); + it('handles signed addition', () => { + expect(addBigNumbers('-5', '3')).toBe('-2'); + expect(addBigNumbers('5', '-3')).toBe('2'); + expect(addBigNumbers('-5', '-3')).toBe('-8'); + expect(addBigNumbers('5', '-5')).toBe('0'); + expect(addBigNumbers('-100', '50')).toBe('-50'); + }); + }); + + describe('mulBigNumbers', () => { + it('multiplies positive numbers', () => { + expect(mulBigNumbers('0', '999')).toBe('0'); + expect(mulBigNumbers('12', '12')).toBe('144'); + expect(mulBigNumbers('123', '456')).toBe('56088'); + expect( + mulBigNumbers('100000000000000000000', '100000000000000000000'), + ).toBe('10000000000000000000000000000000000000000'); + }); + it('handles signed multiplication', () => { + expect(mulBigNumbers('-12', '12')).toBe('-144'); + expect(mulBigNumbers('-12', '-12')).toBe('144'); + expect(mulBigNumbers('12', '0')).toBe('0'); + }); + }); + + describe('divBy2', () => { + it('floor-divides by 2', () => { + expect(divBy2('0')).toBe('0'); + expect(divBy2('2')).toBe('1'); + expect(divBy2('1000')).toBe('500'); + expect(divBy2('1001')).toBe('500'); + expect(divBy2('10000000000000000000000')).toBe('5000000000000000000000'); + }); + }); + + describe('cmpAbs', () => { + it('compares magnitudes', () => { + expect(cmpAbs('5', '5')).toBe(0); + expect(cmpAbs('5', '10')).toBe(-1); + expect(cmpAbs('10', '5')).toBe(1); + expect(cmpAbs('0005', '5')).toBe(0); + }); + }); +}); diff --git a/src/problem1/view.tsx b/src/problem1/view.tsx new file mode 100644 index 0000000000..5786a4c3c7 --- /dev/null +++ b/src/problem1/view.tsx @@ -0,0 +1,336 @@ +import { useMemo, useState } from 'react'; +import { + sum_to_n_a, + sum_to_n_b, + sum_to_n_c, + sum_to_n_d, +} from './lib/sumToN'; +import { solutions, type SolutionId } from './solutions'; +import './style.css'; + +const fns: Record number | string> = { + a: sum_to_n_a as (n: never) => number, + b: sum_to_n_b as (n: never) => number, + c: sum_to_n_c as (n: never) => number, + d: sum_to_n_d as (n: never) => string, +}; + +interface ComputeResult { + value?: string; + time?: number; + error?: string; + warning?: string; +} + +interface TestRow { + pass: boolean; + label: string; + expected: string; + actual: string; +} + +const SMALL_CASES: Array<{ n: number; expected: number }> = [ + { n: 0, expected: 0 }, + { n: 1, expected: 1 }, + { n: 5, expected: 15 }, + { n: 10, expected: 55 }, + { n: 100, expected: 5050 }, + { n: 1000, expected: 500500 }, + { n: -1, expected: -1 }, + { n: -5, expected: -15 }, + { n: -100, expected: -5050 }, +]; + +const BIG_CASES: Array<{ n: string; expected: string }> = [ + { n: '1000000', expected: '500000500000' }, + { n: '100000000000', expected: '5000000000050000000000' }, + { + n: '123456789012345678901234567890', + expected: + '7620789614188397919306039269735013345025385542661216720495', + }, + { n: '-100000000000', expected: '-5000000000050000000000' }, +]; + +// Largest |n| for which n*(n+1)/2 still fits in Number.MAX_SAFE_INTEGER. +// Derived from n*(n+1) <= 2 * MAX_SAFE_INTEGER (= 2^54 - 2): +// floor((sqrt(1 + 8 * MAX_SAFE_INTEGER) - 1) / 2) === 134_217_727. +// At n = 134_217_727 the sum is 9_007_199_187_632_128, still inside the +// safe range. At n = 134_217_728 it overflows. +const MAX_SAFE_N = 134_217_727; + +// Per-solution practical input ceiling. a/b are bounded only by the +// overflow constraint above. c additionally allocates an array of length +// |n|, so we cap it well below the engine's array-length limit to avoid +// "Invalid array length" / out-of-memory. a is also slow at the upper +// end (~1.5s loop), so we mark long runs with a warning in the result. +const PER_SOLUTION_MAX: Record<'a' | 'b' | 'c', number> = { + a: MAX_SAFE_N, + b: MAX_SAFE_N, + c: 1_000_000, +}; + +const SLOW_THRESHOLD = 5_000_000; + +export function Problem1View() { + const [selected, setSelected] = useState('a'); + const [rawInput, setRawInput] = useState('100'); + const [result, setResult] = useState(null); + const [tests, setTests] = useState(null); + + const sol = solutions[selected]; + + const onSelect = (id: SolutionId) => { + setSelected(id); + setResult(null); + setTests(null); + }; + + const onCompute = () => { + const raw = rawInput.trim(); + if (!raw) { + setResult({ error: 'Please enter a value for n.' }); + return; + } + + try { + let arg: number | string; + let slowWarning: string | undefined; + if (selected === 'd') { + arg = raw; + } else { + const num = Number(raw); + if (!Number.isFinite(num) || !Number.isInteger(num)) { + setResult({ + error: `n must be an integer for sum_to_n_${selected}. Switch to sum_to_n_d for arbitrarily large input.`, + }); + return; + } + const limit = PER_SOLUTION_MAX[selected]; + if (Math.abs(num) > limit) { + const reason = + selected === 'c' + ? `sum_to_n_c allocates an array of length |n|, so the practical ceiling is ${limit.toLocaleString()}.` + : `Result would exceed Number.MAX_SAFE_INTEGER. The largest |n| keeping nΒ·(n+1)/2 inside the safe range is ${limit.toLocaleString()}.`; + setResult({ + error: `${reason} Switch to sum_to_n_d for arbitrarily large input.`, + }); + return; + } + if (selected === 'a' && Math.abs(num) > SLOW_THRESHOLD) { + slowWarning = `Heads up: sum_to_n_a loops |n| = ${Math.abs(num).toLocaleString()} times β€” the UI may freeze briefly.`; + } + arg = num; + } + + const t0 = performance.now(); + const value = (fns[selected] as (n: number | string) => number | string)( + arg, + ); + const t1 = performance.now(); + setResult({ + value: String(value), + time: t1 - t0, + warning: slowWarning, + }); + } catch (err) { + setResult({ error: err instanceof Error ? err.message : String(err) }); + } + }; + + const onRunTests = () => { + const fn = fns[selected] as (n: number | string) => number | string; + const cases = + selected === 'd' + ? [ + ...SMALL_CASES.map(({ n, expected }) => ({ + n: String(n), + expected: String(expected), + })), + ...BIG_CASES, + ] + : SMALL_CASES.map(({ n, expected }) => ({ + n: n as number | string, + expected: expected as number | string, + })); + + const rows: TestRow[] = cases.map(({ n, expected }) => { + try { + const actual = fn(n as never); + return { + pass: actual === expected, + label: `sum_to_n_${selected}(${n})`, + expected: String(expected), + actual: String(actual), + }; + } catch (err) { + return { + pass: false, + label: `sum_to_n_${selected}(${n})`, + expected: String(expected), + actual: `threw: ${err instanceof Error ? err.message : String(err)}`, + }; + } + }); + + setTests(rows); + }; + + const testSummary = useMemo(() => { + if (!tests) return null; + const passed = tests.filter((r) => r.pass).length; + return { passed, total: tests.length }; + }, [tests]); + + return ( +
+ {/* ---------------- LEFT: solution picker + detail ---------------- */} +
+

Solutions

+
+ {(Object.keys(solutions) as SolutionId[]).map((id) => ( + + ))} +
+ +
+

{sol.name}

+
+ + Time {sol.time} + + + Space {sol.space} + +
+ +

Explanation

+

{sol.explanation}

+ +

Code

+
+            {sol.code}
+          
+
+
+ + {/* ---------------- RIGHT: compute + tests ---------------- */} +
+

Try it

+
+ setRawInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') onCompute(); + }} + placeholder="Enter n (e.g. 100, -50, or 123456789012345678901234567890)" + autoComplete="off" + className="p1-input mono" + /> +
+
+ {selected === 'd' ? ( + <> + sum_to_n_d accepts arbitrarily long decimal + strings. No upper bound. + + ) : ( + <> + Max |n| for sum_to_n_{selected}:{' '} + + {PER_SOLUTION_MAX[selected].toLocaleString()} + + {selected === 'c' + ? ' (bounded by array memory).' + : ' (keeps result inside Number.MAX_SAFE_INTEGER).'} + + )} +
+
+ + +
+ +

Result

+
+ {!result && ( + No computation yet. + )} + {result?.error && ( + <> +
Error
+
{result.error}
+ + )} + {result?.value !== undefined && !result.error && ( + <> +
{result.value}
+
+ computed in {result.time?.toFixed(3)} ms +
+ {result.warning && ( +
{result.warning}
+ )} + + )} +
+ +

Tests

+ {!tests && ( +
+ Click Run test suite to execute the in-browser test cases. +
+ )} + {testSummary && ( +
+ {testSummary.passed} / {testSummary.total} passed +
+ )} + {tests && ( +
    + {tests.map((r, i) => ( +
  • + {r.pass ? 'βœ“' : 'βœ—'} + {r.label} + + expected {r.expected} + {!r.pass && ( + <> + , got {r.actual} + + )} + +
  • + ))} +
+ )} +
+
+ ); +} diff --git a/src/problem2/api.ts b/src/problem2/api.ts new file mode 100644 index 0000000000..668b089db4 --- /dev/null +++ b/src/problem2/api.ts @@ -0,0 +1,40 @@ +import type { PriceEntry } from './types'; + +const PRICES_URL = 'https://interview.switcheo.com/prices.json'; + +// Fetch the raw price feed. The endpoint returns an array of +// `{ currency, date, price }` entries. The same currency can appear +// multiple times with different timestamps, so the caller is responsible +// for deduplicating (see `dedupeLatest`). +export async function fetchPrices(): Promise { + const res = await fetch(PRICES_URL); + if (!res.ok) { + throw new Error( + `Failed to fetch prices: ${res.status} ${res.statusText}`, + ); + } + const data = (await res.json()) as PriceEntry[]; + if (!Array.isArray(data)) { + throw new Error('Unexpected price feed shape: expected an array.'); + } + return data; +} + +// Keep only the most recent price per currency. Entries without a valid +// numeric price are dropped (per the spec: "tokens that do not [have a +// price] can be omitted"). +export function dedupeLatest(entries: PriceEntry[]): PriceEntry[] { + const latest = new Map(); + for (const entry of entries) { + if (!entry || typeof entry.price !== 'number' || !isFinite(entry.price)) { + continue; + } + const prev = latest.get(entry.currency); + if (!prev || new Date(entry.date) > new Date(prev.date)) { + latest.set(entry.currency, entry); + } + } + return [...latest.values()].sort((a, b) => + a.currency.localeCompare(b.currency), + ); +} diff --git a/src/problem2/balances.ts b/src/problem2/balances.ts new file mode 100644 index 0000000000..89fc2370f5 --- /dev/null +++ b/src/problem2/balances.ts @@ -0,0 +1,62 @@ +// Mock wallet balance store. +// +// Initial balances come from `data/balances.json` so the demo starts in a +// realistic state. Tokens that aren't in the JSON fall back to a +// deterministic hash-derived balance, so every symbol in the live price +// feed still has something to spend. +// +// During the session the balances are mutated by `applySwap` (called from +// the swap form's submit handler). Reloading the page resets to the JSON +// snapshot β€” by design, since we deliberately don't touch localStorage to +// keep the demo reproducible. + +import seedData from './data/balances.json'; + +const seed = seedData as Record; + +const hashSymbol = (symbol: string): number => { + let h = 5381; + for (let i = 0; i < symbol.length; i++) { + h = ((h << 5) + h + symbol.charCodeAt(i)) | 0; + } + return Math.abs(h); +}; + +// Fallback for symbols not listed in balances.json: spread across roughly +// four orders of magnitude so the UI shows a believable mix. +const fallbackBalance = (symbol: string): number => { + const h = hashSymbol(symbol); + const bucket = h % 4; + const mantissa = 1 + ((h >> 4) % 9000) / 1000; + const scales = [0.1, 1, 100, 10_000]; + return Number((mantissa * scales[bucket]).toFixed(4)); +}; + +export const initialBalance = (symbol: string): number => + Object.prototype.hasOwnProperty.call(seed, symbol) + ? seed[symbol] + : fallbackBalance(symbol); + +// Build the initial balance map for the given set of symbols. +export const loadInitialBalances = ( + symbols: string[], +): Record => { + const out: Record = {}; + for (const s of symbols) out[s] = initialBalance(s); + return out; +}; + +// Pure helper used by the view's swap handler: deduct `amountIn` from the +// FROM token and credit `amountOut` to the TO token. Returns a new map +// (immutable update) so React state updates trigger re-render. +export const applySwap = ( + balances: Record, + fromSymbol: string, + amountIn: number, + toSymbol: string, + amountOut: number, +): Record => ({ + ...balances, + [fromSymbol]: Math.max(0, (balances[fromSymbol] ?? 0) - amountIn), + [toSymbol]: (balances[toSymbol] ?? 0) + amountOut, +}); diff --git a/src/problem2/components/TokenIcon.tsx b/src/problem2/components/TokenIcon.tsx new file mode 100644 index 0000000000..c2697a2132 --- /dev/null +++ b/src/problem2/components/TokenIcon.tsx @@ -0,0 +1,51 @@ +import { useState } from 'react'; +import { iconUrl } from '../tokenIcon'; + +interface Props { + symbol: string; + size?: number; +} + +// Renders the Switcheo SVG icon for a token, falling back to a coloured +// letter avatar if the network request fails (not every symbol in the +// price feed has an icon in the repo). +export function TokenIcon({ symbol, size = 28 }: Props) { + const [errored, setErrored] = useState(false); + + if (errored) { + const hue = symbolHue(symbol); + return ( + + {symbol.slice(0, 2).toUpperCase()} + + ); + } + + return ( + {`${symbol} setErrored(true)} + /> + ); +} + +const symbolHue = (symbol: string): number => { + let h = 0; + for (let i = 0; i < symbol.length; i++) { + h = (h * 31 + symbol.charCodeAt(i)) | 0; + } + return Math.abs(h) % 360; +}; diff --git a/src/problem2/components/TokenSelect.tsx b/src/problem2/components/TokenSelect.tsx new file mode 100644 index 0000000000..f25afd27c3 --- /dev/null +++ b/src/problem2/components/TokenSelect.tsx @@ -0,0 +1,141 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import type { Token } from '../types'; +import { TokenIcon } from './TokenIcon'; +import { formatBalance, formatPrice } from '../format'; + +interface Props { + tokens: Token[]; + value: Token | null; + onChange: (token: Token) => void; + excludeSymbol?: string | null; + placeholder?: string; +} + +// Searchable token dropdown. Trigger shows the selected token (icon + +// symbol); opening the panel reveals a search box and a scrollable list. +// The list is filtered case-insensitively against the symbol, and any +// token matching `excludeSymbol` is greyed out and unselectable so the +// user cannot pick the same token on both sides of the swap. +export function TokenSelect({ + tokens, + value, + onChange, + excludeSymbol = null, + placeholder = 'Select a token', +}: Props) { + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(''); + const containerRef = useRef(null); + const inputRef = useRef(null); + + useEffect(() => { + if (!open) { + setQuery(''); + return; + } + const onDocClick = (e: MouseEvent) => { + if ( + containerRef.current && + !containerRef.current.contains(e.target as Node) + ) { + setOpen(false); + } + }; + const onEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape') setOpen(false); + }; + document.addEventListener('mousedown', onDocClick); + document.addEventListener('keydown', onEsc); + // Focus the search input after the dropdown paints. + queueMicrotask(() => inputRef.current?.focus()); + return () => { + document.removeEventListener('mousedown', onDocClick); + document.removeEventListener('keydown', onEsc); + }; + }, [open]); + + const filtered = useMemo(() => { + const q = query.trim().toLowerCase(); + if (!q) return tokens; + return tokens.filter((t) => t.symbol.toLowerCase().includes(q)); + }, [tokens, query]); + + return ( +
+ + + {open && ( +
+
+ setQuery(e.target.value)} + placeholder="Search tokens..." + autoComplete="off" + /> +
+
    + {filtered.length === 0 && ( +
  • No tokens match "{query}"
  • + )} + {filtered.map((t) => { + const disabled = t.symbol === excludeSymbol; + const selected = value?.symbol === t.symbol; + return ( +
  • + +
  • + ); + })} +
+
+ )} +
+ ); +} diff --git a/src/problem2/data/balances.json b/src/problem2/data/balances.json new file mode 100644 index 0000000000..f8567ee8e6 --- /dev/null +++ b/src/problem2/data/balances.json @@ -0,0 +1,30 @@ +{ + "ETH": 2.5, + "WBTC": 0.05, + "USDC": 1500.0, + "USD": 1200.0, + "USDT": 950.0, + "ATOM": 45.2, + "OSMO": 1280.5, + "ARB": 530.0, + "ZIL": 12500.0, + "NEO": 42.18, + "GAS": 60.0, + "BNB": 3.7, + "SWTH": 100000.0, + "BLUR": 5000.0, + "STRD": 320.0, + "STEVMOS": 180.0, + "STOSMO": 410.0, + "STATOM": 90.0, + "STLUNA": 75.0, + "LUNA": 120.0, + "EVMOS": 250.0, + "AKT": 320.0, + "AXLUSDC": 850.0, + "KUJI": 540.0, + "USC": 700.0, + "rATOM": 60.0, + "wstETH": 1.2, + "RATOM": 60.0 +} diff --git a/src/problem2/format.ts b/src/problem2/format.ts new file mode 100644 index 0000000000..8eb7cfed28 --- /dev/null +++ b/src/problem2/format.ts @@ -0,0 +1,26 @@ +// Small display helpers for numbers shown in the swap form. + +const COMPACT_THRESHOLD = 1_000_000; + +export const formatBalance = (n: number): string => { + if (!isFinite(n)) return 'β€”'; + if (n === 0) return '0'; + if (n >= COMPACT_THRESHOLD) { + return n.toLocaleString(undefined, { + notation: 'compact', + maximumFractionDigits: 2, + }); + } + if (n < 0.0001) return n.toExponential(2); + if (n < 1) return n.toPrecision(4); + return n.toLocaleString(undefined, { maximumFractionDigits: 4 }); +}; + +export const formatPrice = (n: number): string => { + if (!isFinite(n) || n === 0) return '$0'; + if (n < 0.0001) return `$${n.toExponential(2)}`; + if (n < 1) return `$${n.toPrecision(4)}`; + return `$${n.toLocaleString(undefined, { maximumFractionDigits: 4 })}`; +}; + +export const formatPct = (n: number): string => `${n.toFixed(2)}%`; diff --git a/src/problem2/index.html b/src/problem2/index.html deleted file mode 100644 index 4058a68bff..0000000000 --- a/src/problem2/index.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - Fancy Form - - - - - - - - -
-
Swap
- - - - - - - -
- - - - diff --git a/src/problem2/problem2.md b/src/problem2/problem2.md new file mode 100644 index 0000000000..df4fba8516 --- /dev/null +++ b/src/problem2/problem2.md @@ -0,0 +1,31 @@ +# Problem 2: Fancy Form + + + +# Task + +Create a currency swap form based on the template provided in the folder. A user would use this form to swap assets from one currency to another. + +*You may use any third party plugin, library, and/or framework for this problem.* + +1. You may add input validation/error messages to make the form interactive. +2. Your submission will be rated on its usage intuitiveness and visual attractiveness. +3. Show us your frontend development and design skills, feel free to totally disregard the provided files for this problem. +4. You may use this [repo](https://github.com/Switcheo/token-icons/tree/main/tokens) for token images, e.g. [SVG image](https://raw.githubusercontent.com/Switcheo/token-icons/main/tokens/SWTH.svg). +5. You may use this [URL](https://interview.switcheo.com/prices.json) for token price information and to compute exchange rates (not every token has a price, those that do not can be omitted). + + + +Please submit your solution using the files provided in the skeletal repo, including any additional files your solution may use. + + \ No newline at end of file diff --git a/src/problem2/script.js b/src/problem2/script.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/problem2/style.css b/src/problem2/style.css index 915af91c72..039614705c 100644 --- a/src/problem2/style.css +++ b/src/problem2/style.css @@ -1,8 +1,459 @@ -body { +/* Problem 2 β€” Fancy Currency Swap */ + +.p2-wrap { display: flex; - flex-direction: row; + justify-content: center; + padding: 16px 0 48px; +} + +.p2-card { + width: 100%; + max-width: 480px; + background: var(--panel); + border: 1px solid var(--border); + border-radius: 18px; + padding: 22px; + box-shadow: var(--shadow-2); + display: flex; + flex-direction: column; + gap: 8px; +} + +.p2-card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; + gap: 10px; + flex-wrap: wrap; +} + +.p2-title { + margin: 0; + font-size: 18px; + font-weight: 600; + color: var(--text-strong); +} + +/* ---------------- slippage ---------------- */ + +.p2-slip { + display: flex; + align-items: center; + gap: 4px; + background: var(--panel-2); + padding: 4px; + border-radius: 8px; + border: 1px solid var(--border); +} + +.p2-slip-label { + font-size: 11px; + color: var(--muted); + padding: 0 6px; + letter-spacing: 0.4px; + text-transform: uppercase; +} + +.p2-slip-btn { + background: transparent; + border: none; + color: var(--muted); + font: inherit; + font-size: 12px; + font-weight: 600; + padding: 5px 9px; + border-radius: 5px; + cursor: pointer; +} + +.p2-slip-btn:hover { + color: var(--text); +} + +.p2-slip-btn.active { + background: var(--accent-bg); + color: var(--text-strong); +} + +.p2-slip-input { + width: 60px; + background: var(--code-bg); + color: var(--text); + border: 1px solid var(--border); + border-radius: 4px; + padding: 4px 6px; + font-size: 12px; + text-align: right; +} + +.p2-slip-input:focus { + outline: none; + border-color: var(--accent); +} + +.p2-slip-input::-webkit-outer-spin-button, +.p2-slip-input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* ---------------- leg (from / to) ---------------- */ + +.p2-leg { + background: var(--panel-2); + border: 1px solid var(--border); + border-radius: 12px; + padding: 14px 16px; + display: flex; + flex-direction: column; + gap: 8px; + transition: border-color 0.1s ease; +} + +.p2-leg:focus-within { + border-color: var(--accent); +} + +.p2-leg-head { + display: flex; + justify-content: space-between; align-items: center; + font-size: 12px; + color: var(--muted); +} + +.p2-balance { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.p2-max { + background: var(--accent-bg); + color: var(--accent-strong); + border: none; + padding: 2px 6px; + border-radius: 3px; + font-size: 10px; + font-weight: 700; + cursor: pointer; + font-family: inherit; +} + +.p2-max:hover { + background: var(--accent); + color: #0a0f1a; +} + +.p2-max:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.p2-leg-body { + display: flex; + align-items: center; + gap: 10px; +} + +.p2-amount { + flex: 1; + min-width: 0; + background: transparent; + border: none; + color: var(--text-strong); + font-size: 26px; + font-weight: 500; + padding: 4px 0; + letter-spacing: -0.02em; +} + +.p2-amount:focus { + outline: none; +} + +.p2-amount.readonly { + cursor: default; +} + +.p2-amount::placeholder { + color: var(--muted); + opacity: 0.4; +} + +.p2-leg-foot { + font-size: 11px; + color: var(--muted); +} + +/* ---------------- flip button ---------------- */ + +.p2-flip-row { + display: flex; justify-content: center; - min-width: 360px; - font-family: Arial, Helvetica, sans-serif; + margin: -10px 0; + position: relative; + z-index: 2; +} + +.p2-flip { + background: var(--panel); + border: 3px solid var(--bg); + color: var(--text); + width: 36px; + height: 36px; + border-radius: 10px; + cursor: pointer; + font-size: 16px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; +} + +.p2-flip:hover:not(:disabled) { + background: var(--accent-bg); + color: var(--accent-strong); + transform: rotate(180deg); +} + +.p2-flip:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* ---------------- summary ---------------- */ + +.p2-summary { + background: var(--panel-2); + border: 1px solid var(--border); + border-radius: 10px; + padding: 12px 14px; + display: flex; + flex-direction: column; + gap: 6px; + margin-top: 4px; +} + +.p2-summary-row { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 12px; + color: var(--muted); +} + +.p2-summary-value { + color: var(--text); +} + +/* ---------------- validation ---------------- */ + +.p2-validation { + color: var(--warn); + font-size: 12.5px; + padding: 8px 12px; + background: rgba(240, 182, 87, 0.08); + border: 1px solid rgba(240, 182, 87, 0.3); + border-radius: 8px; +} + +/* ---------------- submit ---------------- */ + +.p2-submit { + width: 100%; + padding: 14px; + font-size: 15px; + border-radius: 10px; + margin-top: 4px; +} + +/* ---------------- token select ---------------- */ + +.ts { + position: relative; + flex-shrink: 0; +} + +.ts-trigger { + display: flex; + align-items: center; + gap: 8px; + background: var(--panel); + border: 1px solid var(--border); + color: var(--text-strong); + padding: 6px 10px 6px 8px; + border-radius: 999px; + cursor: pointer; + font: inherit; + font-weight: 600; + transition: all 0.1s ease; +} + +.ts-trigger:hover { + border-color: var(--accent); +} + +.ts-trigger.placeholder { + color: var(--muted); + font-weight: 500; +} + +.ts-trigger-symbol { + font-size: 14px; +} + +.ts-trigger-caret { + font-size: 9px; + color: var(--muted); + margin-left: 2px; +} + +.ts-panel { + position: absolute; + top: calc(100% + 6px); + right: 0; + width: 320px; + background: var(--panel); + border: 1px solid var(--border-strong); + border-radius: 10px; + box-shadow: var(--shadow-2); + z-index: 10; + overflow: hidden; + display: flex; + flex-direction: column; + max-height: 380px; +} + +.ts-search-row { + padding: 10px; + border-bottom: 1px solid var(--border); +} + +.ts-search { + width: 100%; + background: var(--code-bg); + border: 1px solid var(--border); + color: var(--text); + padding: 8px 10px; + border-radius: 6px; + font-size: 13px; + font-family: inherit; +} + +.ts-search:focus { + outline: none; + border-color: var(--accent); +} + +.ts-list { + list-style: none; + padding: 6px; + margin: 0; + overflow-y: auto; +} + +.ts-option { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 8px 10px; + background: transparent; + border: none; + border-radius: 6px; + cursor: pointer; + color: var(--text); + font: inherit; + text-align: left; +} + +.ts-option:hover:not(.disabled) { + background: var(--panel-2); +} + +.ts-option.selected { + background: var(--accent-bg); +} + +.ts-option.disabled { + opacity: 0.35; + cursor: not-allowed; +} + +.ts-option-main { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; +} + +.ts-option-symbol { + font-weight: 600; + color: var(--text-strong); + font-size: 13px; +} + +.ts-option-balance { + font-size: 11px; + color: var(--muted); + margin-top: 1px; +} + +.ts-option-price { + font-size: 12px; + color: var(--muted); +} + +.ts-empty { + padding: 16px; + text-align: center; + color: var(--muted); + font-size: 13px; +} + +/* ---------------- token icon ---------------- */ + +.token-icon { + border-radius: 50%; + background: var(--panel-2); +} + +.token-fallback { + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 700; + text-transform: uppercase; + flex-shrink: 0; +} + +/* ---------------- load / error states ---------------- */ + +.p2-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px; + gap: 12px; + color: var(--muted); +} + +.p2-state .spinner { + color: var(--accent); +} + +.p2-state.error code { + background: var(--code-bg); + border: 1px solid var(--code-border); + padding: 8px 12px; + border-radius: 6px; + font-size: 12px; + color: var(--fail); + max-width: 600px; + word-break: break-all; } diff --git a/src/problem2/tokenIcon.ts b/src/problem2/tokenIcon.ts new file mode 100644 index 0000000000..276b5abff8 --- /dev/null +++ b/src/problem2/tokenIcon.ts @@ -0,0 +1,9 @@ +// Switcheo token icon SVGs are served from the `token-icons` repo at +// raw.githubusercontent.com. Not every symbol in the price feed has an +// icon β€” `IconImg` in view.tsx swaps to a fallback letter avatar on error. + +const ICON_BASE = + 'https://raw.githubusercontent.com/Switcheo/token-icons/main/tokens'; + +export const iconUrl = (symbol: string): string => + `${ICON_BASE}/${symbol}.svg`; diff --git a/src/problem2/types.ts b/src/problem2/types.ts new file mode 100644 index 0000000000..6d2b4c7fad --- /dev/null +++ b/src/problem2/types.ts @@ -0,0 +1,23 @@ +// Domain types for the currency swap form. + +export interface PriceEntry { + currency: string; + date: string; + price: number; +} + +export interface Token { + symbol: string; + price: number; + balance: number; + iconUrl: string; +} + +export interface SwapQuote { + amountIn: number; + amountOut: number; + rate: number; + feePct: number; + feeAmount: number; + minReceived: number; +} diff --git a/src/problem2/view.tsx b/src/problem2/view.tsx new file mode 100644 index 0000000000..5b9bbe4d21 --- /dev/null +++ b/src/problem2/view.tsx @@ -0,0 +1,419 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { dedupeLatest, fetchPrices } from './api'; +import { applySwap, loadInitialBalances } from './balances'; +import { iconUrl } from './tokenIcon'; +import { TokenSelect } from './components/TokenSelect'; +import { TokenIcon } from './components/TokenIcon'; +import { formatBalance, formatPct, formatPrice } from './format'; +import type { SwapQuote, Token } from './types'; +import './style.css'; + +const FEE_PCT = 0.003; // 0.3% protocol fee (mock) +const SLIPPAGE_PRESETS = [0.1, 0.5, 1.0]; +const DEFAULT_SLIPPAGE = 0.5; +const SUBMIT_DELAY_MS = 1500; + +interface TokenMeta { + symbol: string; + price: number; + iconUrl: string; +} + +type LoadState = + | { kind: 'loading' } + | { kind: 'error'; message: string } + | { kind: 'ready'; meta: TokenMeta[] }; + +type SubmitState = + | { kind: 'idle' } + | { kind: 'submitting' } + | { kind: 'done'; ok: true; message: string } + | { kind: 'done'; ok: false; message: string }; + +export function Problem2View() { + const [load, setLoad] = useState({ kind: 'loading' }); + const [balances, setBalances] = useState>({}); + const [fromSymbol, setFromSymbol] = useState(null); + const [toSymbol, setToSymbol] = useState(null); + const [amountRaw, setAmountRaw] = useState(''); + const [slippage, setSlippage] = useState(DEFAULT_SLIPPAGE); + const [submit, setSubmit] = useState({ kind: 'idle' }); + + const loadPrices = useCallback(() => { + setLoad({ kind: 'loading' }); + fetchPrices() + .then((entries) => { + const deduped = dedupeLatest(entries); + const meta: TokenMeta[] = deduped.map((e) => ({ + symbol: e.currency, + price: e.price, + iconUrl: iconUrl(e.currency), + })); + setLoad({ kind: 'ready', meta }); + setBalances(loadInitialBalances(meta.map((m) => m.symbol))); + // Pick a sensible default pair so the form has something to show. + const eth = meta.find((m) => m.symbol === 'ETH'); + const usdc = meta.find((m) => m.symbol === 'USDC'); + setFromSymbol((eth ?? meta[0])?.symbol ?? null); + setToSymbol((usdc ?? meta[1] ?? meta[0])?.symbol ?? null); + }) + .catch((err: unknown) => { + setLoad({ + kind: 'error', + message: err instanceof Error ? err.message : String(err), + }); + }); + }, []); + + useEffect(() => { + loadPrices(); + }, [loadPrices]); + + // Auto-dismiss the submit toast after a couple of seconds. + useEffect(() => { + if (submit.kind !== 'done') return; + const t = setTimeout(() => setSubmit({ kind: 'idle' }), 3000); + return () => clearTimeout(t); + }, [submit]); + + // Derive the user-facing token list by joining static price metadata + // with the live (mutable) balance map. + const tokens: Token[] = useMemo(() => { + if (load.kind !== 'ready') return []; + return load.meta.map((m) => ({ + symbol: m.symbol, + price: m.price, + iconUrl: m.iconUrl, + balance: balances[m.symbol] ?? 0, + })); + }, [load, balances]); + + const fromToken = tokens.find((t) => t.symbol === fromSymbol) ?? null; + const toToken = tokens.find((t) => t.symbol === toSymbol) ?? null; + + const amount = parseFloat(amountRaw); + const amountValid = + amountRaw !== '' && Number.isFinite(amount) && amount > 0; + + const quote: SwapQuote | null = useMemo(() => { + if (!fromToken || !toToken || !amountValid) return null; + const rate = fromToken.price / toToken.price; + const grossOut = amount * rate; + const feeAmount = grossOut * FEE_PCT; + const amountOut = grossOut - feeAmount; + const minReceived = amountOut * (1 - slippage / 100); + return { + amountIn: amount, + amountOut, + rate, + feePct: FEE_PCT * 100, + feeAmount, + minReceived, + }; + }, [fromToken, toToken, amount, amountValid, slippage]); + + const validation = useMemo(() => { + if (!fromToken || !toToken) return 'Select both tokens to continue.'; + if (fromToken.symbol === toToken.symbol) + return 'From and to must be different tokens.'; + if (amountRaw === '') return null; + if (!Number.isFinite(amount)) return 'Amount must be a valid number.'; + if (amount <= 0) return 'Amount must be greater than zero.'; + if (amount > fromToken.balance) + return `Insufficient balance. Max: ${formatBalance(fromToken.balance)} ${fromToken.symbol}.`; + return null; + }, [fromToken, toToken, amount, amountRaw]); + + const canSubmit = + !!fromToken && + !!toToken && + fromToken.symbol !== toToken.symbol && + amountValid && + amount <= fromToken.balance && + submit.kind !== 'submitting'; + + const onFlip = () => { + if (!fromToken || !toToken) return; + setFromSymbol(toToken.symbol); + setToSymbol(fromToken.symbol); + setAmountRaw(''); + }; + + const onMax = () => { + if (!fromToken) return; + setAmountRaw(String(fromToken.balance)); + }; + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!canSubmit || !fromToken || !toToken || !quote) return; + setSubmit({ kind: 'submitting' }); + // Mock backend: pretend to send the swap, then update the local + // balance map so the FROM token is debited and the TO token credited. + setTimeout(() => { + setBalances((prev) => + applySwap( + prev, + fromToken.symbol, + quote.amountIn, + toToken.symbol, + quote.amountOut, + ), + ); + setSubmit({ + kind: 'done', + ok: true, + message: `Swapped ${formatBalance(quote.amountIn)} ${fromToken.symbol} β†’ ${formatBalance(quote.amountOut)} ${toToken.symbol}.`, + }); + setAmountRaw(''); + }, SUBMIT_DELAY_MS); + }; + + if (load.kind === 'loading') { + return ( +
+
+

Loading Switcheo prices…

+
+ ); + } + + if (load.kind === 'error') { + return ( +
+

Failed to load price feed.

+ {load.message} + +
+ ); + } + + return ( +
+
+
+

Swap

+ +
+ + {/* ---------------- FROM panel ---------------- */} +
+
+ From + {fromToken && ( + + Balance: {formatBalance(fromToken.balance)} {fromToken.symbol}{' '} + + + )} +
+
+ { + const v = e.target.value; + if (v === '' || /^[0-9]*\.?[0-9]*$/.test(v)) setAmountRaw(v); + }} + /> + setFromSymbol(t.symbol)} + excludeSymbol={toSymbol} + /> +
+ {fromToken && amountValid && ( +
+ β‰ˆ {formatPrice(amount * fromToken.price)} +
+ )} +
+ + {/* ---------------- Swap direction ---------------- */} +
+ +
+ + {/* ---------------- TO panel ---------------- */} +
+
+ To + {toToken && ( + + Balance: {formatBalance(toToken.balance)} {toToken.symbol} + + )} +
+
+ + setToSymbol(t.symbol)} + excludeSymbol={fromSymbol} + /> +
+ {toToken && quote && ( +
+ β‰ˆ {formatPrice(quote.amountOut * toToken.price)} +
+ )} +
+ + {/* ---------------- Rate + fee summary ---------------- */} + {quote && fromToken && toToken && ( +
+ + 1 {fromToken.symbol} ={' '} + {formatBalance(quote.rate)}{' '} + {toToken.symbol} + + } + /> + + {formatBalance(quote.feeAmount)} {toToken.symbol} + + } + /> + + {formatBalance(quote.minReceived)} {toToken.symbol} + + } + /> +
+ )} + + {/* ---------------- Validation message ---------------- */} + {validation && ( +
{validation}
+ )} + + {/* ---------------- Submit ---------------- */} + +
+ + {/* ---------------- Toast ---------------- */} + {submit.kind === 'done' && ( +
+ {submit.ok ? ( + fromToken && toToken ? ( + <> + + {submit.message} + + ) : ( + {submit.message} + ) + ) : ( + {submit.message} + )} +
+ )} +
+ ); +} + +// --------------- subcomponents --------------- + +function SlippageControl({ + value, + onChange, +}: { + value: number; + onChange: (v: number) => void; +}) { + const isPreset = SLIPPAGE_PRESETS.includes(value); + return ( +
+ Slippage + {SLIPPAGE_PRESETS.map((p) => ( + + ))} + { + const v = parseFloat(e.target.value); + if (Number.isFinite(v) && v > 0) onChange(v); + }} + /> +
+ ); +} + +function SummaryRow({ + label, + value, +}: { + label: string; + value: React.ReactNode; +}) { + return ( +
+ {label} + {value} +
+ ); +} diff --git a/src/problem3/.keep b/src/problem3/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/problem3/Refactored.tsx b/src/problem3/Refactored.tsx new file mode 100644 index 0000000000..fabfe564f6 --- /dev/null +++ b/src/problem3/Refactored.tsx @@ -0,0 +1,73 @@ +import { useMemo } from 'react'; +import { + type BoxProps, + type Blockchain, + type FormattedWalletBalance, + type WalletBalance, + WalletRow, + classes, + usePrices, + useWalletBalances, +} from './mocks'; + +// --- pure, dependency-free helpers hoisted out of the component --- + +const PRIORITY: Record = { + Osmosis: 100, + Ethereum: 50, + Arbitrum: 30, + Zilliqa: 20, + Neo: 20, +}; + +const getPriority = (blockchain: Blockchain): number => + PRIORITY[blockchain] ?? -99; + +const AMOUNT_DECIMALS = 4; + +// `Props` adds nothing of its own, so use `BoxProps` directly. `children` +// was destructured and never used in the original; leaving it on `...rest` +// keeps the component a transparent wrapper if a parent passes it. +export function WalletPage(props: BoxProps) { + const balances = useWalletBalances(); + const prices = usePrices(); + + // Single memo: filter + sort + format + compute usdValue. Schwartzian + // transform means getPriority/price lookup runs once per row, not once + // per sort comparison. Depends on both `balances` and `prices` because + // both feed the final output. + const rows = useMemo(() => { + return balances + .filter( + (b: WalletBalance) => + b.amount > 0 && getPriority(b.blockchain) > -99, + ) + .map((b) => ({ + balance: b, + priority: getPriority(b.blockchain), + price: prices[b.currency] ?? 0, + })) + .sort((a, b) => b.priority - a.priority) + .map(({ balance, price }) => ({ + ...balance, + formatted: balance.amount.toFixed(AMOUNT_DECIMALS), + usdValue: price * balance.amount, + })); + }, [balances, prices]); + + return ( +
+ {rows.map((row) => ( + + ))} +
+ ); +} diff --git a/src/problem3/analysis.ts b/src/problem3/analysis.ts new file mode 100644 index 0000000000..07465b6e95 --- /dev/null +++ b/src/problem3/analysis.ts @@ -0,0 +1,200 @@ +// Catalogue of issues in the original WalletPage. Each entry is rendered +// as a card in the Problem 3 tab. + +export type Severity = 'bug' | 'perf' | 'anti-pattern' | 'style'; + +export interface Issue { + id: number; + title: string; + severity: Severity; + body: string; +} + +export const issues: Issue[] = [ + { + id: 1, + title: 'Filter references an undefined variable `lhsPriority`', + severity: 'bug', + body: + 'Inside `.filter(...)` the condition reads `lhsPriority`, a name ' + + 'that is never declared in scope. The intended variable is the ' + + '`balancePriority` that was computed one line above. In strict mode ' + + 'this is a ReferenceError; in non-strict (legacy) mode it is silently ' + + '`undefined`, so `undefined > -99` is `false` and every item is ' + + 'rejected before the inner amount check ever runs.', + }, + { + id: 2, + title: 'Filter logic is inverted β€” keeps balances with `amount <= 0`', + severity: 'bug', + body: + 'The inner branch returns `true` when `balance.amount <= 0`. A swap / ' + + 'wallet view should drop empty or negative balances and keep the ' + + 'positive ones, so the comparison should be `amount > 0`. As written, ' + + 'the only way an item would survive the filter is if priority were ' + + 'valid AND the user had zero or negative funds.', + }, + { + id: 3, + title: '`formattedBalances` is computed but never used', + severity: 'bug', + body: + 'After sorting, the code maps `sortedBalances` into `formattedBalances` ' + + 'with a `formatted` field, but `rows` then maps `sortedBalances` again β€” ' + + 'not `formattedBalances`. As a result `balance.formatted` is `undefined` ' + + 'at render time, which gets passed as `formattedAmount` to `WalletRow`. ' + + 'Either consume `formattedBalances` or fold the format step into the ' + + 'main pipeline.', + }, + { + id: 4, + title: 'Sort comparator returns `undefined` for equal priorities', + severity: 'bug', + body: + 'When `leftPriority === rightPriority`, neither branch executes and ' + + 'the comparator implicitly returns `undefined`. The Array.prototype.sort ' + + 'spec requires a number; V8 currently treats this as 0 but the behaviour ' + + 'is fragile and TypeScript with `noImplicitReturns` flags it. Always ' + + 'return `0` (or a tiebreaker like alphabetical currency) explicitly.', + }, + { + id: 5, + title: '`WalletBalance` type is missing the `blockchain` field', + severity: 'bug', + body: + 'The component reads `balance.blockchain` to compute priority, but the ' + + 'interface only declares `currency` and `amount`. With TS strict mode ' + + 'this is a compile error; without strict mode it silently typed as ' + + '`any`. The refactor adds `blockchain: Blockchain` to the interface so ' + + 'the lookup is type-safe.', + }, + { + id: 6, + title: '`getPriority(blockchain: any)` defeats the type system', + severity: 'anti-pattern', + body: + 'Typing the parameter as `any` lets any string (or non-string) flow ' + + 'through unchecked. Define a `Blockchain` union of the supported chains ' + + 'and type the parameter as that, so adding a new chain becomes a ' + + 'compile-time TODO instead of a silent `-99`.', + }, + { + id: 7, + title: '`getPriority` is re-declared on every render', + severity: 'perf', + body: + 'The function is pure and closes over nothing from the component scope. ' + + 'Defining it inside the component allocates a new function reference on ' + + 'every render, which doesn\'t matter for `getPriority` itself but does ' + + 'matter if you ever pass it as a prop or memo dependency. Hoist it to ' + + 'module scope (or wrap in `useCallback` if it truly needs state).', + }, + { + id: 8, + title: '`getPriority` runs O(n log n) times during sort', + severity: 'perf', + body: + 'The sort comparator calls `getPriority` twice per comparison, and a ' + + 'sort performs O(n log n) comparisons, so the function is invoked ' + + 'O(n log n) times. Precompute the priority once per item with a ' + + '`.map` (a "Schwartzian transform"), sort by the precomputed number, ' + + 'and the lookup cost drops to O(n).', + }, + { + id: 9, + title: '`useMemo` depends on `prices` but the memoised value does not use it', + severity: 'perf', + body: + 'The dependency array `[balances, prices]` causes `sortedBalances` to ' + + 'recompute whenever the price feed ticks, even though filter/sort only ' + + 'read `balance.amount` and `balance.blockchain`. Either drop `prices` ' + + 'from the deps OR consume it inside the memo (the refactor does the ' + + 'latter by including USD value).', + }, + { + id: 10, + title: '`formattedBalances` is not memoised', + severity: 'perf', + body: + '`sortedBalances.map(...)` runs on every render, even when nothing ' + + 'relevant changed. Move the format step into the same `useMemo` as the ' + + 'sort so the entire derived array is cached together.', + }, + { + id: 11, + title: 'Using array `index` as React `key`', + severity: 'anti-pattern', + body: + 'When the underlying list is sorted or items are inserted / removed, ' + + 'index-based keys force React to re-mount components instead of ' + + 'reusing them, which kills local state and animation continuity. Use a ' + + 'stable identifier β€” here `balance.currency` is naturally unique per ' + + 'row.', + }, + { + id: 12, + title: '`prices[balance.currency]` may be undefined β†’ NaN in the UI', + severity: 'bug', + body: + '`prices[unknownCurrency]` returns `undefined`, and `undefined * 2` is ' + + '`NaN`. The UI then shows "$NaN". The refactor either filters out ' + + 'rows with no price OR falls back to 0 (the demo chooses the latter to ' + + 'keep the row visible).', + }, + { + id: 13, + title: 'Empty `interface Props extends BoxProps {}`', + severity: 'anti-pattern', + body: + 'The interface declares no members of its own β€” it is a redundant ' + + 'alias for `BoxProps`. Either use `BoxProps` directly, or `type Props ' + + '= BoxProps` if you want a local alias.', + }, + { + id: 14, + title: '`children` destructured but never rendered', + severity: 'anti-pattern', + body: + 'The original pulls `children` out of `props` and then discards it. ' + + 'If the parent passes JSX between the tags it silently disappears. ' + + 'Either render `{children}` or include it in `...rest` so the wrapping ' + + '`
` keeps acting as a transparent box.', + }, + { + id: 15, + title: '`balance.amount.toFixed()` without a precision argument', + severity: 'bug', + body: + 'Calling `toFixed()` with no argument defaults to 0 decimals, so ' + + '`(1.2345).toFixed()` returns `"1"` and the displayed balance loses ' + + 'precision. Pick an explicit decimal count appropriate for the asset ' + + '(the refactor uses 4).', + }, + { + id: 16, + title: 'Row items typed as `FormattedWalletBalance` but actually `WalletBalance`', + severity: 'bug', + body: + 'The `rows = sortedBalances.map(...)` callback annotates `balance` as ' + + '`FormattedWalletBalance`, but `sortedBalances` is `WalletBalance[]` ' + + 'because `formattedBalances` was never plumbed in. TS strict mode ' + + 'would flag this as an unsafe cast; without strict mode it hides the ' + + 'runtime "formatted is undefined" bug.', + }, + { + id: 17, + title: '`usdValue` is recomputed in the render path', + severity: 'perf', + body: + 'Each render recalculates `prices[balance.currency] * balance.amount` ' + + 'for every row. Move it into the memoised pipeline so the multiplication ' + + 'only fires when balances or prices actually change.', + }, +]; + +export const SEVERITY_LABEL: Record = { + bug: 'Bug', + perf: 'Perf', + 'anti-pattern': 'Anti-pattern', + style: 'Style', +}; diff --git a/src/problem3/mocks.tsx b/src/problem3/mocks.tsx new file mode 100644 index 0000000000..b16480a896 --- /dev/null +++ b/src/problem3/mocks.tsx @@ -0,0 +1,98 @@ +// Stand-ins for symbols the original code pulled from external modules: +// `useWalletBalances`, `usePrices`, `WalletRow`, `BoxProps`, `classes`. +// They let the refactored component actually render inside the demo tab. + +import type React from 'react'; + +export type Blockchain = + | 'Osmosis' + | 'Ethereum' + | 'Arbitrum' + | 'Zilliqa' + | 'Neo'; + +export interface WalletBalance { + currency: string; + amount: number; + blockchain: Blockchain; +} + +export interface FormattedWalletBalance extends WalletBalance { + formatted: string; + usdValue: number; +} + +export type BoxProps = React.HTMLAttributes; + +export const classes = { + row: 'p3-row', +} as const; + +// --- demo data (mock hook outputs) --- + +const FIXTURE_BALANCES: WalletBalance[] = [ + { currency: 'ETH', amount: 2.4137, blockchain: 'Ethereum' }, + { currency: 'OSMO', amount: 1280.5, blockchain: 'Osmosis' }, + { currency: 'ARB', amount: 530.0, blockchain: 'Arbitrum' }, + { currency: 'ZIL', amount: 12_500, blockchain: 'Zilliqa' }, + { currency: 'NEO', amount: 42.18, blockchain: 'Neo' }, + // amount <= 0 β†’ should be filtered out + { currency: 'GAS', amount: 0, blockchain: 'Neo' }, + // priority -99 β†’ should be filtered out + { currency: 'UNKNOWN', amount: 10, blockchain: 'Osmosis' }, +]; + +const FIXTURE_PRICES: Record = { + ETH: 2_385.0, + OSMO: 0.78, + ARB: 0.91, + ZIL: 0.018, + NEO: 11.4, + GAS: 4.2, + // UNKNOWN intentionally has no price +}; + +// React Hooks rules require a stable identity so React treats them as +// hooks; using regular functions is fine because they have no state. +export const useWalletBalances = (): WalletBalance[] => FIXTURE_BALANCES; +export const usePrices = (): Record => FIXTURE_PRICES; + +interface WalletRowProps { + className?: string; + amount: number; + usdValue: number; + formattedAmount: string; + currency: string; + blockchain: Blockchain; +} + +export function WalletRow({ + className, + amount, + usdValue, + formattedAmount, + currency, + blockchain, +}: WalletRowProps) { + return ( +
+
+ {currency} + {blockchain} +
+
+ + {formattedAmount}{' '} + ({amount}) + + + $ + {usdValue.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + +
+
+ ); +} diff --git a/src/problem3/original.tsx.txt b/src/problem3/original.tsx.txt new file mode 100644 index 0000000000..78678160fe --- /dev/null +++ b/src/problem3/original.tsx.txt @@ -0,0 +1,81 @@ +interface WalletBalance { + currency: string; + amount: number; +} +interface FormattedWalletBalance { + currency: string; + amount: number; + formatted: string; +} + +interface Props extends BoxProps { + +} +const WalletPage: React.FC = (props: Props) => { + const { children, ...rest } = props; + const balances = useWalletBalances(); + const prices = usePrices(); + + const getPriority = (blockchain: any): number => { + switch (blockchain) { + case 'Osmosis': + return 100 + case 'Ethereum': + return 50 + case 'Arbitrum': + return 30 + case 'Zilliqa': + return 20 + case 'Neo': + return 20 + default: + return -99 + } + } + + const sortedBalances = useMemo(() => { + return balances.filter((balance: WalletBalance) => { + const balancePriority = getPriority(balance.blockchain); + if (lhsPriority > -99) { + if (balance.amount <= 0) { + return true; + } + } + return false + }).sort((lhs: WalletBalance, rhs: WalletBalance) => { + const leftPriority = getPriority(lhs.blockchain); + const rightPriority = getPriority(rhs.blockchain); + if (leftPriority > rightPriority) { + return -1; + } else if (rightPriority > leftPriority) { + return 1; + } + }); + }, [balances, prices]); + + const formattedBalances = sortedBalances.map((balance: WalletBalance) => { + return { + ...balance, + formatted: balance.amount.toFixed() + } + }) + + const rows = sortedBalances.map((balance: FormattedWalletBalance, index: number) => { + const usdValue = prices[balance.currency] * balance.amount; + return ( + + ) + }) + + return ( +
+ {rows} +
+ ) +} diff --git a/src/problem3/problem3.md b/src/problem3/problem3.md new file mode 100644 index 0000000000..1bbd6f0194 --- /dev/null +++ b/src/problem3/problem3.md @@ -0,0 +1,99 @@ +# Problem 3: Messy React + + + +# Task + +List out the computational inefficiencies and anti-patterns found in the code block below. + +1. This code block uses + 1. ReactJS with TypeScript. + 2. Functional components. + 3. React Hooks +2. You should also provide a refactored version of the code, but more points are awarded to accurately stating the issues and explaining correctly how to improve them. + +interface WalletBalance { + currency: string; + amount: number; +} +interface FormattedWalletBalance { + currency: string; + amount: number; + formatted: string; +} + +interface Props extends BoxProps { + +} +const WalletPage: React.FC = (props: Props) => { + const { children, ...rest } = props; + const balances = useWalletBalances(); + const prices = usePrices(); + + const getPriority = (blockchain: any): number => { + switch (blockchain) { + case 'Osmosis': + return 100 + case 'Ethereum': + return 50 + case 'Arbitrum': + return 30 + case 'Zilliqa': + return 20 + case 'Neo': + return 20 + default: + return -99 + } + } + + const sortedBalances = useMemo(() => { + return balances.filter((balance: WalletBalance) => { + const balancePriority = getPriority(balance.blockchain); + if (lhsPriority > -99) { + if (balance.amount <= 0) { + return true; + } + } + return false + }).sort((lhs: WalletBalance, rhs: WalletBalance) => { + const leftPriority = getPriority(lhs.blockchain); + const rightPriority = getPriority(rhs.blockchain); + if (leftPriority > rightPriority) { + return -1; + } else if (rightPriority > leftPriority) { + return 1; + } + }); + }, [balances, prices]); + + const formattedBalances = sortedBalances.map((balance: WalletBalance) => { + return { + ...balance, + formatted: balance.amount.toFixed() + } + }) + + const rows = sortedBalances.map((balance: FormattedWalletBalance, index: number) => { + const usdValue = prices[balance.currency] * balance.amount; + return ( + + ) + }) + + return ( +
+ {rows} +
+ ) +} \ No newline at end of file diff --git a/src/problem3/style.css b/src/problem3/style.css new file mode 100644 index 0000000000..d052ee867f --- /dev/null +++ b/src/problem3/style.css @@ -0,0 +1,292 @@ +/* Problem 3 β€” Messy React */ + +.p3 { + display: flex; + flex-direction: column; + gap: 20px; +} + +.p3-section-head { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 16px; + margin-bottom: 14px; + flex-wrap: wrap; +} + +.p3-section-head h2 { + text-transform: none; + letter-spacing: 0; + font-size: 16px; + color: var(--text-strong); + margin: 0 0 4px; +} + +.p3-section-blurb { + margin: 0; + font-size: 12.5px; + color: var(--muted); + max-width: 720px; +} + +.p3-section-blurb code { + background: var(--code-bg); + border: 1px solid var(--code-border); + padding: 0 4px; + border-radius: 3px; + font-size: 11.5px; + color: var(--text); +} + +/* ---------------- filter ---------------- */ + +.p3-filter, +.p3-layout-toggle { + display: flex; + gap: 4px; + background: var(--panel-2); + padding: 4px; + border-radius: 8px; + border: 1px solid var(--border); + flex-wrap: wrap; +} + +.p3-filter-btn { + background: transparent; + border: none; + color: var(--muted); + font: inherit; + font-size: 12px; + font-weight: 600; + padding: 5px 10px; + border-radius: 5px; + cursor: pointer; +} + +.p3-filter-btn:hover { + color: var(--text); +} + +.p3-filter-btn.active { + background: var(--accent-bg); + color: var(--text-strong); +} + +/* ---------------- issues list ---------------- */ + +.p3-issue-list { + list-style: none; + padding: 0; + margin: 0; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); + gap: 12px; +} + +.p3-issue { + background: var(--panel-2); + border: 1px solid var(--border); + border-left: 3px solid var(--border-strong); + border-radius: 8px; + padding: 14px 16px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.p3-issue.sev-bug { + border-left-color: var(--fail); +} +.p3-issue.sev-perf { + border-left-color: var(--warn); +} +.p3-issue.sev-anti-pattern { + border-left-color: var(--accent); +} +.p3-issue.sev-style { + border-left-color: var(--muted); +} + +.p3-issue-head { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.p3-issue-num { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-weight: 700; + color: var(--muted); + font-size: 12px; +} + +.p3-sev-badge { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 2px 7px; + border-radius: 3px; + background: var(--code-bg); + color: var(--muted); +} + +.p3-sev-badge.sev-bug { + background: rgba(255, 104, 104, 0.16); + color: var(--fail); +} +.p3-sev-badge.sev-perf { + background: rgba(240, 182, 87, 0.16); + color: var(--warn); +} +.p3-sev-badge.sev-anti-pattern { + background: var(--accent-bg); + color: var(--accent-strong); +} + +.p3-issue-title { + margin: 0; + font-size: 13.5px; + font-weight: 600; + color: var(--text-strong); + flex: 1; + min-width: 200px; +} + +.p3-issue-body { + margin: 0; + font-size: 12.5px; + line-height: 1.6; + color: var(--text); +} + +/* ---------------- code grid ---------------- */ + +.p3-code-grid { + display: grid; + gap: 16px; +} + +.p3-code-grid.split { + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); +} + +.p3-code-grid.stacked { + grid-template-columns: 1fr; +} + +@media (max-width: 1080px) { + .p3-code-grid.split { + grid-template-columns: 1fr; + } +} + +.p3-code-block { + display: flex; + flex-direction: column; + background: var(--code-bg); + border: 1px solid var(--code-border); + border-radius: 8px; + overflow: hidden; + min-width: 0; +} + +.p3-code-block.original { + border-color: rgba(255, 104, 104, 0.3); +} + +.p3-code-block.refactored { + border-color: rgba(68, 196, 102, 0.3); +} + +.p3-code-label { + padding: 8px 14px; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.6px; + color: var(--muted); + border-bottom: 1px solid var(--code-border); + background: var(--panel-2); +} + +.p3-code-block.original .p3-code-label { + color: var(--fail); +} + +.p3-code-block.refactored .p3-code-label { + color: var(--pass); +} + +.p3-code { + margin: 0; + padding: 14px 16px; + overflow: auto; + font-size: 12px; + line-height: 1.55; + max-height: 520px; +} + +/* ---------------- demo ---------------- */ + +.p3-demo { + background: var(--panel-2); + border: 1px solid var(--border); + border-radius: 10px; + padding: 14px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.p3-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: var(--panel); + border: 1px solid var(--border); + border-radius: 8px; + gap: 16px; +} + +.p3-row-main { + display: flex; + flex-direction: column; + gap: 2px; +} + +.p3-row-symbol { + font-size: 14px; + font-weight: 600; + color: var(--text-strong); +} + +.p3-row-chain { + font-size: 11px; + color: var(--muted); +} + +.p3-row-numbers { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; +} + +.p3-row-amount { + font-size: 13px; + color: var(--text); +} + +.p3-row-amount-raw { + color: var(--muted); + font-size: 11px; +} + +.p3-row-usd { + font-size: 12px; + color: var(--muted); +} diff --git a/src/problem3/view.tsx b/src/problem3/view.tsx new file mode 100644 index 0000000000..c4f13c7dc5 --- /dev/null +++ b/src/problem3/view.tsx @@ -0,0 +1,154 @@ +import { useMemo, useState } from 'react'; +import { issues, SEVERITY_LABEL, type Severity } from './analysis'; +import { WalletPage } from './Refactored'; +import originalSource from './original.tsx.txt?raw'; +import refactoredSource from './Refactored.tsx?raw'; +import './style.css'; + +type CodeLayout = 'split' | 'stacked'; +type SeverityFilter = Severity | 'all'; + +export function Problem3View() { + const [layout, setLayout] = useState('split'); + const [filter, setFilter] = useState('all'); + + const filtered = useMemo( + () => + filter === 'all' + ? issues + : issues.filter((i) => i.severity === filter), + [filter], + ); + + const counts = useMemo(() => { + const c: Record = { + bug: 0, + perf: 0, + 'anti-pattern': 0, + style: 0, + }; + for (const i of issues) c[i.severity]++; + return c; + }, []); + + return ( +
+ {/* ---------------- Section 1: Issues ---------------- */} +
+
+
+

Issues found

+

+ {issues.length} distinct issues across bugs, performance, and + anti-patterns. +

+
+
+ {(['all', 'bug', 'perf', 'anti-pattern'] as SeverityFilter[]).map( + (f) => ( + + ), + )} +
+
+ +
    + {filtered.map((issue) => ( +
  1. +
    + #{issue.id} + + {SEVERITY_LABEL[issue.severity]} + +

    {issue.title}

    +
    +

    {issue.body}

    +
  2. + ))} +
+
+ + {/* ---------------- Section 2: Side-by-side code ---------------- */} +
+
+
+

Original vs Refactored

+

+ Left: the messy code from the prompt. Right: refactored version + addressing every issue listed above. +

+
+
+ + +
+
+ +
+ + +
+
+ + {/* ---------------- Section 3: Live demo ---------------- */} +
+
+
+

Refactored component β€” live demo

+

+ Rendered with mocked useWalletBalances,{' '} + usePrices, WalletRow, and{' '} + BoxProps so the page actually mounts. Rows below + are produced by the refactored pipeline, sorted by chain + priority (Osmosis > Ethereum > Arbitrum > Zilliqa = Neo). +

+
+
+ +
+ +
+
+
+ ); +} + +function CodeBlock({ + label, + source, + flavour, +}: { + label: string; + source: string; + flavour: 'original' | 'refactored'; +}) { + return ( +
+
{label}
+
+        {source}
+      
+
+ ); +} diff --git a/src/problem4/.keep b/src/problem4/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/problem5/.keep b/src/problem5/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/shell.css b/src/shell.css new file mode 100644 index 0000000000..808be7279f --- /dev/null +++ b/src/shell.css @@ -0,0 +1,294 @@ +:root { + color-scheme: dark; + --bg: #0b0f17; + --bg-elev: #111827; + --panel: #161e2e; + --panel-2: #1c2638; + --border: #2a3447; + --border-strong: #3a465e; + --text: #e6edf6; + --text-strong: #f3f7fd; + --muted: #95a3b8; + --accent: #6aa7ff; + --accent-strong: #88baff; + --accent-bg: rgba(106, 167, 255, 0.14); + --pass: #44c466; + --fail: #ff6868; + --warn: #f0b657; + --code-bg: #060a13; + --code-border: #1f2a3d; + --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.4); + --shadow-2: 0 8px 24px rgba(0, 0, 0, 0.35); + --radius: 10px; + --radius-sm: 6px; +} + +* { + box-sizing: border-box; +} + +html, +body, +#root { + margin: 0; + padding: 0; + background: var(--bg); + color: var(--text); + min-height: 100vh; +} + +body { + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Inter, Oxygen, + Ubuntu, Cantarell, sans-serif; + font-size: 14px; + line-height: 1.55; + -webkit-font-smoothing: antialiased; +} + +code, +pre, +.mono { + font-family: + ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace; +} + +a { + color: var(--accent); + text-decoration: none; +} +a:hover { + color: var(--accent-strong); + text-decoration: underline; +} + +/* ---------------- app shell ---------------- */ + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 28px 32px 48px; +} + +.app-header { + margin-bottom: 24px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} + +.app-title { + display: flex; + align-items: baseline; + gap: 12px; + margin-bottom: 16px; +} + +.app-title h1 { + margin: 0; + font-size: 22px; + font-weight: 600; + color: var(--text-strong); +} + +.app-title-tag { + font-size: 11px; + font-weight: 700; + letter-spacing: 1.5px; + color: var(--accent); + background: var(--accent-bg); + padding: 4px 8px; + border-radius: 4px; + text-transform: uppercase; +} + +.app-tabs { + display: flex; + gap: 4px; + margin-bottom: 10px; + flex-wrap: wrap; +} + +.app-tab { + padding: 9px 14px; + border-radius: 6px; + font-size: 13px; + color: var(--muted); + font-weight: 500; + border: 1px solid transparent; + transition: all 0.1s ease; +} + +.app-tab:hover { + color: var(--text); + background: var(--panel); + text-decoration: none; +} + +.app-tab.active { + color: var(--text-strong); + background: var(--accent-bg); + border-color: var(--accent); +} + +.app-subtitle { + margin: 0; + color: var(--muted); + font-size: 13px; +} + +.app-main { + min-height: 60vh; +} + +.app-footer { + margin-top: 32px; + padding-top: 16px; + border-top: 1px solid var(--border); + color: var(--muted); + font-size: 12px; +} + +.app-footer code { + background: var(--code-bg); + border: 1px solid var(--code-border); + padding: 1px 6px; + border-radius: 4px; + font-size: 11.5px; +} + +/* ---------------- shared panel ---------------- */ + +.panel { + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 20px; + min-width: 0; +} + +.panel h2, +.panel h3 { + color: var(--text-strong); + margin: 0 0 10px; +} + +.panel h2 { + font-size: 11px; + font-weight: 600; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.6px; +} + +.panel h2:not(:first-child) { + margin-top: 22px; +} + +/* ---------------- shared buttons ---------------- */ + +.btn { + border: none; + padding: 10px 18px; + border-radius: var(--radius-sm); + font-weight: 600; + font-family: inherit; + font-size: 13px; + cursor: pointer; + transition: all 0.1s ease; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; +} + +.btn:disabled { + cursor: not-allowed; + opacity: 0.55; +} + +.btn-primary { + background: var(--accent); + color: #0a0f1a; +} + +.btn-primary:not(:disabled):hover { + background: var(--accent-strong); +} + +.btn-secondary { + background: transparent; + border: 1px solid var(--border-strong); + color: var(--text); +} + +.btn-secondary:not(:disabled):hover { + border-color: var(--accent); + color: var(--text-strong); +} + +.btn-ghost { + background: transparent; + color: var(--muted); + padding: 6px 10px; +} + +.btn-ghost:hover { + color: var(--text); + background: var(--panel-2); +} + +/* ---------------- spinner ---------------- */ + +.spinner { + width: 14px; + height: 14px; + border: 2px solid currentColor; + border-top-color: transparent; + border-radius: 50%; + animation: spin 0.7s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* ---------------- toast ---------------- */ + +.toast { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + background: var(--panel); + border: 1px solid var(--border-strong); + padding: 12px 18px; + border-radius: 8px; + box-shadow: var(--shadow-2); + font-size: 13px; + display: flex; + align-items: center; + gap: 10px; + animation: toast-in 0.2s ease; + z-index: 9999; +} + +.toast.toast-success { + border-color: var(--pass); +} + +.toast.toast-error { + border-color: var(--fail); +} + +@keyframes toast-in { + from { + opacity: 0; + transform: translateX(-50%) translateY(10px); + } + to { + opacity: 1; + transform: translateX(-50%) translateY(0); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..310a55fa09 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "skipLibCheck": true, + "esModuleInterop": true, + "isolatedModules": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "useDefineForClassFields": true, + "types": ["vite/client"], + "noEmit": true + }, + "include": ["src", "vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000000..2bb2d8d10d --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + open: true, + }, + build: { + outDir: 'dist', + emptyOutDir: true, + sourcemap: true, + }, + test: { + environment: 'node', + globals: false, + include: ['src/**/*.test.{ts,tsx}'], + }, +}); From 2cb68180faf61356d3e59444c19e40ba9e4b68e3 Mon Sep 17 00:00:00 2001 From: 0367592801 Date: Sat, 30 May 2026 17:05:49 +0700 Subject: [PATCH 2/2] docs: add live demo link at top of README Co-Authored-By: Claude Opus 4.7 --- readme.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/readme.md b/readme.md index 1a24faf8af..b924a9cdad 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,9 @@ # 99Tech Code Challenge #1 +> **Live demo:** **https://code-challenge-lake.vercel.app/** β€” want a quick +> look without cloning? Open the link, all three problems are reachable +> from the tabs at the top (`#/problem1`, `#/problem2`, `#/problem3`). + Solutions to the three problems, bundled as one React + Vite + TypeScript app with a tab per problem.