From a0d960082aeb6e2b7397f39890b6750c8864ec51 Mon Sep 17 00:00:00 2001 From: Immaculate0606 Date: Mon, 29 Jun 2026 09:21:17 +0100 Subject: [PATCH 1/3] feat: favorite APIs and Favorites-only filter --- package-lock.json | 615 ++------------------------ src/components/ApiCard.tsx | 64 ++- src/components/FiltersBottomSheet.tsx | 6 + src/components/FiltersSidebar.tsx | 22 + src/hooks/useFavorites.ts | 19 + src/pages/MarketplacePage.tsx | 82 ++-- 6 files changed, 186 insertions(+), 622 deletions(-) create mode 100644 src/hooks/useFavorites.ts diff --git a/package-lock.json b/package-lock.json index 5b37466..343f03c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,24 +25,18 @@ "vitest": "^1.6.0" } }, - "node_modules/@anthropic-ai/sdk": { - "version": "0.91.1", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", - "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", - "license": "MIT", + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "json-schema-to-ts": "^3.1.1" - }, - "bin": { - "anthropic-ai-sdk": "bin/cli" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } + "engines": { + "node": ">=6.0.0" } }, "node_modules/@asamuzakjp/css-color": { @@ -304,6 +298,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -357,15 +352,12 @@ "node": ">=6.9.0" } }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.1.90" - } + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", @@ -873,348 +865,14 @@ "node": ">=12" } }, - "node_modules/@inquirer/ansi": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.7.tgz", - "integrity": "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==", - "license": "MIT", - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - } - }, - "node_modules/@inquirer/checkbox": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.1.tgz", - "integrity": "sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==", - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^2.0.7", - "@inquirer/core": "^11.2.1", - "@inquirer/figures": "^2.0.7", - "@inquirer/type": "^4.0.7" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/confirm": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.1.1.tgz", - "integrity": "sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.2.1", - "@inquirer/type": "^4.0.7" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/core": { - "version": "11.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.2.1.tgz", - "integrity": "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==", - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^2.0.7", - "@inquirer/figures": "^2.0.7", - "@inquirer/type": "^4.0.7", - "cli-width": "^4.1.0", - "fast-wrap-ansi": "^0.2.0", - "mute-stream": "^3.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/editor": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.2.2.tgz", - "integrity": "sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.2.1", - "@inquirer/external-editor": "^3.0.3", - "@inquirer/type": "^4.0.7" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/expand": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.1.tgz", - "integrity": "sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.2.1", - "@inquirer/type": "^4.0.7" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/external-editor": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.3.tgz", - "integrity": "sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==", - "license": "MIT", - "dependencies": { - "chardet": "^2.1.1", - "iconv-lite": "^0.7.2" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@inquirer/figures": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.7.tgz", - "integrity": "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==", - "license": "MIT", - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - } - }, - "node_modules/@inquirer/input": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.2.tgz", - "integrity": "sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.2.1", - "@inquirer/type": "^4.0.7" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/number": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.1.1.tgz", - "integrity": "sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.2.1", - "@inquirer/type": "^4.0.7" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/password": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.1.1.tgz", - "integrity": "sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==", - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^2.0.7", - "@inquirer/core": "^11.2.1", - "@inquirer/type": "^4.0.7" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/prompts": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.5.2.tgz", - "integrity": "sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==", - "license": "MIT", - "dependencies": { - "@inquirer/checkbox": "^5.2.1", - "@inquirer/confirm": "^6.1.1", - "@inquirer/editor": "^5.2.2", - "@inquirer/expand": "^5.1.1", - "@inquirer/input": "^5.1.2", - "@inquirer/number": "^4.1.1", - "@inquirer/password": "^5.1.1", - "@inquirer/rawlist": "^5.3.1", - "@inquirer/search": "^4.2.1", - "@inquirer/select": "^5.2.1" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/rawlist": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.1.tgz", - "integrity": "sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.2.1", - "@inquirer/type": "^4.0.7" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/search": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.2.1.tgz", - "integrity": "sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.2.1", - "@inquirer/figures": "^2.0.7", - "@inquirer/type": "^4.0.7" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/select": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.1.tgz", - "integrity": "sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==", - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^2.0.7", - "@inquirer/core": "^11.2.1", - "@inquirer/figures": "^2.0.7", - "@inquirer/type": "^4.0.7" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/type": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", - "integrity": "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==", + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, "license": "MIT", "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "node": ">=8" } }, "node_modules/@jest/schemas": { @@ -2012,6 +1670,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2033,12 +1692,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, "node_modules/aria-query": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", @@ -2281,12 +1934,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chardet": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", - "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", - "license": "MIT" - }, "node_modules/check-error": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", @@ -2300,30 +1947,6 @@ "node": "*" } }, - "node_modules/cli-table3": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", - "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", - "license": "MIT", - "dependencies": { - "string-width": "^4.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "@colors/colors": "1.5.0" - } - }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2357,14 +1980,12 @@ "node": ">= 0.8" } }, - "node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", - "license": "MIT", - "engines": { - "node": ">=20" - } + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" }, "node_modules/confbox": { "version": "0.1.8", @@ -2593,12 +2214,6 @@ "dev": true, "license": "ISC" }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -2765,30 +2380,6 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/fast-string-truncated-width": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", - "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", - "license": "MIT" - }, - "node_modules/fast-string-width": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", - "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", - "license": "MIT", - "dependencies": { - "fast-string-truncated-width": "^3.0.2" - } - }, - "node_modules/fast-wrap-ansi": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", - "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", - "license": "MIT", - "dependencies": { - "fast-string-width": "^3.0.2" - } - }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -3252,15 +2843,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -3498,28 +3080,6 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, - "node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/jsdom": { "version": "24.1.3", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.3.tgz", @@ -3574,19 +3134,6 @@ "node": ">=6" } }, - "node_modules/json-schema-to-ts": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", - "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "ts-algebra": "^2.0.0" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3600,40 +3147,6 @@ "node": ">=6" } }, - "node_modules/lint": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/lint/-/lint-1.2.2.tgz", - "integrity": "sha512-rh59UJG+tlLWS+dIFOID1DF+/O7VwNKiT0+jIT12l+udnYqBRUVE0oMx+sf3IN2pwmdQuViHHj8xR02O0DdCvQ==", - "license": "Apache-2.0", - "dependencies": { - "@anthropic-ai/sdk": "^0.91.0", - "@inquirer/prompts": "^8.4.2", - "chalk": "^5.4.1", - "cli-table3": "^0.6.5", - "commander": "^14.0.3", - "js-yaml": "^4.1.1", - "nanospinner": "^1.2.2", - "prettier": "^3.5.3" - }, - "bin": { - "lint": "dist/index.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/lint/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/local-pkg": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", @@ -3846,15 +3359,6 @@ "dev": true, "license": "MIT" }, - "node_modules/mute-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", - "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -3874,15 +3378,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/nanospinner": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/nanospinner/-/nanospinner-1.2.2.tgz", - "integrity": "sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA==", - "license": "MIT", - "dependencies": { - "picocolors": "^1.1.1" - } - }, "node_modules/node-releases": { "version": "2.0.36", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", @@ -4083,6 +3578,7 @@ "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/pkg-types": { @@ -4143,21 +3639,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/prettier": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.1.tgz", - "integrity": "sha512-ppiDo2CSwexck1eyZUwJHg/N3nf1+6IRCv7W/VJ5vaLnVCmB7+3CdRfMwoCHBBX6xTrREDTksZ4OZl5SSf4zXA==", - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -4392,6 +3873,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, "license": "MIT" }, "node_modules/saxes": { @@ -4570,6 +4052,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -4616,32 +4099,6 @@ "node": ">= 0.4" } }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -4766,12 +4223,6 @@ "node": ">=18" } }, - "node_modules/ts-algebra": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", - "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", - "license": "MIT" - }, "node_modules/type-detect": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", diff --git a/src/components/ApiCard.tsx b/src/components/ApiCard.tsx index 71ad9f3..60e5b07 100644 --- a/src/components/ApiCard.tsx +++ b/src/components/ApiCard.tsx @@ -11,6 +11,7 @@ import Skeleton from "./Skeleton"; import TagChip from "./TagChip"; import { formatPrice } from "../utils/format"; import { useCollections } from "../state/collectionsStore"; +import { useFavorites } from "../hooks/useFavorites"; import type { APIItem } from "../data/mockApis"; import RatingHistogram from "./RatingHistogram"; import { useCompareStore, compareStore } from "../state/compareStore"; @@ -104,6 +105,60 @@ export function ApiCardSkeleton() { // ─── Save-to-collection popover ─────────────────────────────────────────────── +// ─── Favorite button ───────────────────────────────────────────────────────── + +interface FavoriteButtonProps { + endpointId: string; + isFavorite: boolean; + onToggle: (id: string) => void; +} + +function FavoriteButton({ endpointId, isFavorite, onToggle }: FavoriteButtonProps) { + const btnRef = useRef(null); + + return ( + + ); +} + // ─── Bookmark button ───────────────────────────────────────────────────────── interface BookmarkButtonProps { @@ -347,6 +402,7 @@ export default function ApiCard({ const uptimePercent = api.uptimePercent; const isCompact = density === "compact"; + const { isFavorite, toggleFavorite } = useFavorites(); const comparedApis = useCompareStore(); const isCompared = comparedApis.some(item => item.id === api.id); const canCompare = isCompared || comparedApis.length < 3; @@ -386,6 +442,12 @@ export default function ApiCard({ {/* Absolutely-positioned bookmark button in the top-right corner */} + + {/* Compare button - absolutely positioned, top-left */} + + ))} + + ); + } + + return ( +
+ {tiers.map((tier) => ( +
+ {tier.isRecommended && ( +
+ Recommended +
+ )} + +
+

{tier.name}

+
{tier.price}
+

{tier.description}

+
+ +
+ {tier.features.map((feature) => ( +
+ {feature.included ? ( + + ) : ( + + )} + + {feature.label} + +
+ ))} +
+ + +
+ ))} +
+ ); +} diff --git a/src/pages/ApiDetailPage.tsx b/src/pages/ApiDetailPage.tsx index cd13cfc..ac1ce3c 100644 --- a/src/pages/ApiDetailPage.tsx +++ b/src/pages/ApiDetailPage.tsx @@ -13,6 +13,7 @@ import type { Review } from "../data/mockApis"; import EmptyState from "../components/EmptyState"; import { formatPrice } from "../utils/format"; import { Icons } from "../utils/icons"; +import PricingTierTable, { type PricingTier } from "../components/PricingTierTable"; import HealthTimeline from "../components/HealthTimeline"; import { API_BASE_URL, LOADING_DELAY_MS } from "../config/constants"; import { @@ -41,8 +42,8 @@ type TabType = | "reviews"; export default function ApiDetailPage({ onBack }: Props) { -const [, setTab] = useState("overview"); - //const [requests, setRequests] = useState(1000); + const [tab, setTab] = useState("overview"); + const [requests, setRequests] = useState(1000); const [isLoading, setIsLoading] = useState(true); const { showToast } = useToast(); @@ -385,8 +386,9 @@ getApiData().then(console.log).catch(console.error); - // const estimatedCost = (n: number) => - // `$${(n * (api.pricePerRequest ?? 0)).toFixed(2)}`; + const estimatedCost = (n: number) => + `$${(n * (api.pricePerRequest ?? 0)).toFixed(2)}`; + return (
@@ -641,98 +643,46 @@ getApiData().then(console.log).catch(console.error); tabIndex={0} >

Pricing Plans

-
-
-
- Standard -
-
- {`$${formatPrice(api.pricePerRequest ?? 0)}`}{" "} - - / call - -
-

- Perfect for startups and scaling applications. Pay - only for what you use. -

-
    -
  • - ✅ Unlimited Throughput -
  • -
  • - ✅ 99.9% Uptime SLA -
  • -
  • - ✅ Community Support -
  • -
-
-
-
- Enterprise -
-
Custom
-

- For high-volume needs requiring dedicated - infrastructure and support. -

-
    -
  • - ✅ Dedicated Node -
  • -
  • - ✅ 24/7 Phone Support -
  • -
  • - ✅ Custom Rate Limits -
  • -
- -
-
+ ({ label: f, included: true })) || [ + { label: "Standard Support", included: false }, + { label: "High Rate Limits", included: false }, + ], + ctaLabel: "Get Started", + }, + { + name: "Pro", + price: `$${formatPrice(api.pricePerRequest ?? 0)}`, + description: "Ideal for production-grade applications.", + features: api.features?.map((f) => ({ label: f, included: true })) || [ + { label: "Standard Support", included: true }, + { label: "High Rate Limits", included: true }, + ], + ctaLabel: "Upgrade Now", + isRecommended: true, + }, + { + name: "Enterprise", + price: "Custom", + description: "Tailored for large-scale, high-volume needs.", + features: [ + ...(api.features?.map((f) => ({ label: f, included: true })) || []), + { label: "Dedicated Support", included: true }, + { label: "Custom SLA", included: true }, + { label: "Dedicated Infrastructure", included: true }, + ], + ctaLabel: "Contact Sales", + }, + ]} + /> -
-

Cost Calculator

+
+

Cost Calculator

Estimate your monthly billing based on projected request volume. From 4af0439e8acddc6b7be6d1ddac66a626d3589196 Mon Sep 17 00:00:00 2001 From: Immaculate0606 Date: Mon, 29 Jun 2026 10:44:29 +0100 Subject: [PATCH 3/3] feat: compare APIs tray and drawer --- src/App.tsx | 20 +++-- src/components/ApiCard.tsx | 54 ++++++------ src/components/CompareDrawer.css | 144 +++++++++++++++++++++++++++++-- src/components/CompareDrawer.tsx | 55 ++++++++---- src/components/CompareTray.css | 109 +++++++++++++++++++++++ src/components/CompareTray.tsx | 52 +++++++++++ src/pages/MarketplacePage.tsx | 55 ++++++------ src/state/compareStore.ts | 37 ++++++-- 8 files changed, 437 insertions(+), 89 deletions(-) create mode 100644 src/components/CompareTray.css create mode 100644 src/components/CompareTray.tsx diff --git a/src/App.tsx b/src/App.tsx index f53678f..36c36a8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState, useCallback } from 'react'; import { Routes, Route, NavLink, useNavigate, useLocation } from 'react-router-dom'; import { ThemeToggle } from './ThemeToggle'; import ApiUsage from './ApiUsage'; @@ -8,7 +8,6 @@ import ServerError from './components/ServerError'; import useDocumentTitle from "./hooks/useDocumentTitle"; import NotFound from './components/NotFound'; import { startRouteLoading, stopRouteLoading } from './hooks/useRouteLoading'; -import useDocumentTitle from './hooks/useDocumentTitle'; import { formatUsdc, formatUsdShortcut } from './utils/format'; import { EXPLORER_BASE_URL, @@ -16,6 +15,8 @@ import { NETWORK_FEE, PRESET_AMOUNTS, } from "./config/constants"; +import CompareDrawer from './components/CompareDrawer'; +import CompareTray from './components/CompareTray'; type DepositStage = "input" | "approving" | "pending" | "confirmed" | "failed"; type DemoOutcome = "confirmed" | "failed"; @@ -751,10 +752,17 @@ function App() { - setIsShortcutsModalOpen(false)} - /> + setIsShortcutsModalOpen(false)} + /> + + + +

+ ); +} + {isDepositOpen && (
item.id === api.id); const canCompare = isCompared || comparedApis.length < 3; @@ -448,31 +448,33 @@ export default function ApiCard({ onToggle={toggleFavorite} /> - {/* Compare button - absolutely positioned, top-left */} - + {/* Pin button - absolutely positioned, top-left */} + +
(null); - // Sync drawer visibility with items + // Handle ESC to close useEffect(() => { - if (comparedApis.length > 0) { - setIsOpen(true); - } else { - setIsOpen(false); - } - }, [comparedApis.length]); + const handleKey = (e: KeyboardEvent) => { + if (e.key === "Escape" && isOpen) { + compareStore.setOpen(false); + drawerRef.current?.focus(); + } + }; + document.addEventListener("keydown", handleKey); + return () => document.removeEventListener("keydown", handleKey); + }, [isOpen]); const handleRemove = (id: string, name: string) => { compareStore.removeApi(id); setAnnouncement(`Removed ${name} from comparison.`); - // Clear announcement after a moment to allow re-announcing setTimeout(() => setAnnouncement(""), 3000); }; @@ -31,29 +33,44 @@ export default function CompareDrawer() { setTimeout(() => setAnnouncement(""), 3000); }; + if (!isOpen) return null; + return ( <>
{announcement}
-
-
+
compareStore.setOpen(false)}> +
e.stopPropagation()} + tabIndex={-1} + >
-

Compare APIs

- +

Compare APIs

+
+ + +
- {comparedApis.length === 0 ? ( + {apis.length === 0 ? (
Select APIs to compare them.
) : (
- {comparedApis.map((api) => ( + {apis.map((api) => (
+
+ ))} +
+ +
+
+ ); +} diff --git a/src/pages/MarketplacePage.tsx b/src/pages/MarketplacePage.tsx index 0858f76..d8e6425 100644 --- a/src/pages/MarketplacePage.tsx +++ b/src/pages/MarketplacePage.tsx @@ -18,9 +18,12 @@ import { persistDensityPreference, type DensityPreference, } from "../utils/density"; -import CompareDrawer from "../components/CompareDrawer"; +import { useCompareStore } from "../state/compareStore"; export default function MarketplacePage(): JSX.Element { + const { apis } = useCompareStore(); + const isTrayVisible = apis.length > 0; + useDocumentTitle( "Marketplace – Callora", "Explore APIs on the Callora marketplace, discover and integrate APIs for your applications.", @@ -346,8 +349,12 @@ export default function MarketplacePage(): JSX.Element { /> -
-
+
+
+
{filtered.length === 0 ? ( <>Showing 0 of 0 APIs @@ -431,27 +438,23 @@ export default function MarketplacePage(): JSX.Element { {/* Mobile bottom-sheet — only rendered when open */} setShowFiltersMobile(false)} - resultCount={filtered.length} - selectedCategories={selectedCategories} - toggleCategory={toggleCategory} - minPrice={minPrice} - maxPrice={maxPrice} - setMinPrice={setMinPrice} - setMaxPrice={setMaxPrice} - popularity={popularity} - setPopularity={setPopularity} - clearFilters={clearFilters} - favoritesOnly={favoritesOnly} - toggleFavoritesOnly={() => setFavoritesOnly(!favoritesOnly)} - triggerRef={filtersTriggerRef} - /> - - - - {/* Compare Drawer */} - -
- ); + open={showFiltersMobile} + onClose={() => setShowFiltersMobile(false)} + resultCount={filtered.length} + selectedCategories={selectedCategories} + toggleCategory={toggleCategory} + minPrice={minPrice} + maxPrice={maxPrice} + setMinPrice={setMinPrice} + setMaxPrice={setMaxPrice} + popularity={popularity} + setPopularity={setPopularity} + clearFilters={clearFilters} + favoritesOnly={favoritesOnly} + toggleFavoritesOnly={() => setFavoritesOnly(!favoritesOnly)} + triggerRef={filtersTriggerRef} + /> +
+ ); } + diff --git a/src/state/compareStore.ts b/src/state/compareStore.ts index 0b983fc..835bafa 100644 --- a/src/state/compareStore.ts +++ b/src/state/compareStore.ts @@ -1,11 +1,17 @@ import { useSyncExternalStore } from "react"; import type { APIItem } from "../data/mockApis"; -type CompareState = APIItem[]; +type CompareState = { + apis: APIItem[]; + isOpen: boolean; +}; const STORAGE_KEY = "callora_compare_state"; -let state: CompareState = []; +let state: CompareState = { + apis: [], + isOpen: false, +}; try { const stored = localStorage.getItem(STORAGE_KEY); @@ -26,20 +32,37 @@ function emitChange() { export const compareStore = { addApi(api: APIItem) { - if (state.length >= 3) return; - if (state.some((item) => item.id === api.id)) return; + if (state.apis.length >= 3) return; + if (state.apis.some((item) => item.id === api.id)) return; - state = [...state, api]; + state = { + ...state, + apis: [...state.apis, api], + }; localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); emitChange(); }, removeApi(apiId: string) { - state = state.filter((item) => item.id !== apiId); + state = { + ...state, + apis: state.apis.filter((item) => item.id !== apiId), + }; + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); + emitChange(); + }, + setOpen(open: boolean) { + state = { + ...state, + isOpen: open, + }; localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); emitChange(); }, clear() { - state = []; + state = { + apis: [], + isOpen: false, + }; localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); emitChange(); },