From d295ea7a0ea475737b49a07e6761b1a18763f806 Mon Sep 17 00:00:00 2001 From: Nabeelahh Date: Wed, 27 May 2026 10:23:05 +0100 Subject: [PATCH] proper pagination --- package.json | 2 +- pnpm-lock.yaml | 368 +++++++++++++++++++++++--- src/app/api/properties/route.ts | 2 +- src/app/properties/page.tsx | 63 ++++- src/components/PropertyPagination.tsx | 249 +++++++++++++++++ src/components/SearchResults.tsx | 65 ++--- src/hooks/usePaginationParams.ts | 67 +++++ src/hooks/usePropertySearchQuery.ts | 7 + src/lib/propertyService.ts | 22 +- 9 files changed, 740 insertions(+), 105 deletions(-) create mode 100644 src/components/PropertyPagination.tsx create mode 100644 src/hooks/usePaginationParams.ts diff --git a/package.json b/package.json index 47e825c8..6c5791bc 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "@tanstack/react-query-devtools": "^5.100.2", "@tanstack/react-virtual": "^3.13.24", "@wagmi/connectors": "^7.1.2", - "@wagmi/core": "^3.2.2", + "@wagmi/core": "3.4.0", "@walletconnect/web3-provider": "^1.8.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf7e206a..044c66c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -106,10 +106,10 @@ importers: version: 3.13.24(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@wagmi/connectors': specifier: ^7.1.2 - version: 7.2.1(@coinbase/wallet-sdk@4.3.7(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(@metamask/sdk@0.33.1(bufferutil@4.1.0)(utf-8-validate@5.0.10))(@wagmi/core@3.4.6(@tanstack/query-core@5.100.5)(@types/react@19.2.14)(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.5))(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)))(typescript@5.9.3)(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)) + version: 7.2.1(@coinbase/wallet-sdk@4.3.7(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(@metamask/sdk@0.33.1(bufferutil@4.1.0)(utf-8-validate@5.0.10))(@wagmi/core@3.4.0(@tanstack/query-core@5.100.5)(@types/react@19.2.14)(ox@0.14.20(typescript@5.9.3)(zod@4.3.6))(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)))(typescript@5.9.3)(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)) '@wagmi/core': - specifier: ^3.2.2 - version: 3.4.6(@tanstack/query-core@5.100.5)(@types/react@19.2.14)(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.5))(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)) + specifier: 3.4.0 + version: 3.4.0(@tanstack/query-core@5.100.5)(@types/react@19.2.14)(ox@0.14.20(typescript@5.9.3)(zod@4.3.6))(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)) '@walletconnect/web3-provider': specifier: ^1.8.0 version: 1.8.0(@babel/core@7.29.0)(bufferutil@4.1.0)(utf-8-validate@5.0.10) @@ -220,7 +220,7 @@ importers: version: 4.3.6 zustand: specifier: ^5.0.10 - version: 5.0.12(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)) + version: 5.0.12(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.4.0(react@19.2.5)) devDependencies: '@axe-core/playwright': specifier: ^4.11.2 @@ -293,7 +293,7 @@ importers: version: 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@vitest/browser-playwright': specifier: ^4.1.2 - version: 4.1.5(bufferutil@4.1.0)(playwright@1.59.1)(utf-8-validate@5.0.10)(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1))(vitest@4.1.5) + version: 4.1.5(bufferutil@4.1.0)(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3))(playwright@1.59.1)(utf-8-validate@5.0.10)(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1))(vitest@4.1.5) '@vitest/coverage-v8': specifier: ^4.1.2 version: 4.1.5(@vitest/browser@4.1.5)(vitest@4.1.5) @@ -321,6 +321,9 @@ importers: jest-environment-jsdom: specifier: ^29.7.0 version: 29.7.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + msw: + specifier: ^2.13.6 + version: 2.14.6(@types/node@20.19.39)(typescript@5.9.3) playwright: specifier: ^1.58.2 version: 1.59.1 @@ -344,7 +347,7 @@ importers: version: 8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1) vitest: specifier: ^4.1.2 - version: 4.1.5(@types/node@20.19.39)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(jsdom@20.0.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)) + version: 4.1.5(@types/node@20.19.39)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(jsdom@20.0.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)) packages: @@ -1002,6 +1005,41 @@ packages: cpu: [x64] os: [win32] + '@inquirer/ansi@2.0.6': + resolution: {integrity: sha512-I/INw4sHGlVZ/afZOckpLiDP9SmbMl1g/GCqeHjLw1Afw/0PlRs2tRFgTGWmdI0hoNuWZn3y2iHNmG1vyECyQQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + + '@inquirer/confirm@6.1.0': + resolution: {integrity: sha512-USpeB76eqK7yGricDlGAupxWlp4a59qpeZOoNWaxO/nJln7agpJveyNkQ1d5u8YXG6TOqxZtQpKPORQQDrdVsA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@11.2.0': + resolution: {integrity: sha512-joR1YS2sI0us+9d0I8ViqFbrRLONO8CFTuyvBX4ZVBSch+VsZiugUABdrhBXXJR1VyEzvpz5SQCix3keETQ58g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@2.0.6': + resolution: {integrity: sha512-dsZgQtH2t5Q6ah3aPbZbeEZAxsD9qQu0DXf01AltuEfRTm+NoLN6+rLVbr+4edeEbNCp/wBNM6mALRWtsQpfkw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + + '@inquirer/type@4.0.6': + resolution: {integrity: sha512-J+9tdxOskuYuGjsvGaq00AamhDgjR7anhEW2dP4QdQpFCMPngCeC/bCYWQ5NsMWZRdsy53is7kAHb/+7cwDk2g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@ioredis/commands@1.5.1': resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} @@ -1174,6 +1212,10 @@ packages: resolution: {integrity: sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==} engines: {node: '>=16.0.0'} + '@mswjs/interceptors@0.41.9': + resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} + engines: {node: '>=18'} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -1301,6 +1343,18 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/deferred-promise@3.0.0': + resolution: {integrity: sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@oxc-project/types@0.127.0': resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} @@ -2538,6 +2592,9 @@ packages: '@types/secp256k1@4.0.7': resolution: {integrity: sha512-Rcvjl6vARGAKRO6jHeKMatGrvOMGrR/AR11N1x2LqintPCyDZ7NBhrh238Z2VZc7aM7KIwnFpFQ7fnfK4H/9Qw==} + '@types/set-cookie-parser@2.4.10': + resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} + '@types/sinonjs__fake-timers@8.1.1': resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==} @@ -2547,6 +2604,9 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/tmp@0.2.6': resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==} @@ -2905,6 +2965,21 @@ packages: typescript: optional: true + '@wagmi/core@3.4.0': + resolution: {integrity: sha512-EU5gDsUp5t7+cuLv12/L8hfyWfCIKsBNiiBqpOqxZJxvAcAiQk4xFe2jMgaQPqApc3Omvxrk032M8AQ4N0cQeg==} + peerDependencies: + '@tanstack/query-core': '>=5.0.0' + ox: '>=0.11.1' + typescript: '>=5.7.3' + viem: 2.x + peerDependenciesMeta: + '@tanstack/query-core': + optional: true + ox: + optional: true + typescript: + optional: true + '@wagmi/core@3.4.6': resolution: {integrity: sha512-wDZpRfzQo6NJj770mt23HdeU9O0MDO3cnxVP7tP/1HL7DLqOGMN3hADIc0wEF51ejrpnJlGLf8hS1qb2ZAzqJA==} peerDependencies: @@ -3484,6 +3559,10 @@ packages: resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} engines: {node: '>=8'} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -3565,6 +3644,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} @@ -4305,6 +4388,15 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -4525,6 +4617,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphql@16.14.0: + resolution: {integrity: sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + har-schema@2.0.0: resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} engines: {node: '>=4'} @@ -4572,6 +4668,9 @@ packages: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} + headers-polyfill@5.0.1: + resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} + hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} @@ -4805,6 +4904,9 @@ packages: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -5482,6 +5584,20 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msw@2.14.6: + resolution: {integrity: sha512-ALe+N10S72cyx94cMcy3Zs4HhXCj35sgeAL4c+WTvKi0zWnbd8/h0lcFqv0mb2P+aSgAdD7p9HzvA0DiUPxsyg==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + + mute-stream@4.0.0: + resolution: {integrity: sha512-gSrprq0fJ3EiOErzjdIZrjysVVmJ4uu1QWfCDss5LypA5OXvrMje5Ym5z6V6RLyJ2eF87lasX7t6a0AnFvZblg==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -5630,6 +5746,9 @@ packages: ospath@1.2.2: resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -5710,6 +5829,9 @@ packages: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -6110,6 +6232,9 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + rettime@0.11.11: + resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -6206,6 +6331,9 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-cookie-parser@3.1.0: + resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -6264,6 +6392,10 @@ packages: signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + sirv@3.0.2: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} @@ -6341,6 +6473,10 @@ packages: standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} @@ -6357,6 +6493,9 @@ packages: prettier: optional: true + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + strict-uri-encode@2.0.0: resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} engines: {node: '>=4'} @@ -6483,6 +6622,10 @@ packages: os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] hasBin: true + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + tailwind-merge@3.5.0: resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} @@ -6535,10 +6678,17 @@ packages: tldts-core@6.1.86: resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + tldts-core@7.4.0: + resolution: {integrity: sha512-/mb9kRld+x1sIMXxWNOAp5m6C+D4GrAORWlJkOJ5dElvxdN1eutz/o7qHLp9gFvDF4Y3/L2xeScoxz6AbEo8rQ==} + tldts@6.1.86: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true + tldts@7.4.0: + resolution: {integrity: sha512-yHBe+zVfzNZ3QfTPW/Z6KK1G2t340gFjMHqI/4KKSt/abzYydzuCnpqdaF5gCCABby+9Yfbj59oR5F2Fd5CBzg==} + hasBin: true + tmp@0.2.5: resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} engines: {node: '>=14.14'} @@ -6573,6 +6723,10 @@ packages: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -6645,6 +6799,10 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} + type-fest@5.6.0: + resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} + engines: {node: '>=20'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -6701,6 +6859,9 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + untildify@4.0.0: resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} engines: {node: '>=8'} @@ -7783,6 +7944,33 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true + '@inquirer/ansi@2.0.6': {} + + '@inquirer/confirm@6.1.0(@types/node@20.19.39)': + dependencies: + '@inquirer/core': 11.2.0(@types/node@20.19.39) + '@inquirer/type': 4.0.6(@types/node@20.19.39) + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/core@11.2.0(@types/node@20.19.39)': + dependencies: + '@inquirer/ansi': 2.0.6 + '@inquirer/figures': 2.0.6 + '@inquirer/type': 4.0.6(@types/node@20.19.39) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 4.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/figures@2.0.6': {} + + '@inquirer/type@4.0.6(@types/node@20.19.39)': + optionalDependencies: + '@types/node': 20.19.39 + '@ioredis/commands@1.5.1': {} '@istanbuljs/load-nyc-config@1.1.0': @@ -8126,6 +8314,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@mswjs/interceptors@0.41.9': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.10.0 @@ -8216,6 +8413,17 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/deferred-promise@3.0.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + '@oxc-project/types@0.127.0': {} '@paulmillr/qr@0.2.1': {} @@ -9067,10 +9275,10 @@ snapshots: '@storybook/icons': 2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) storybook: 10.3.5(@testing-library/dom@10.4.1)(bufferutil@4.1.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(utf-8-validate@5.0.10) optionalDependencies: - '@vitest/browser': 4.1.5(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1))(vitest@4.1.5) - '@vitest/browser-playwright': 4.1.5(bufferutil@4.1.0)(playwright@1.59.1)(utf-8-validate@5.0.10)(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1))(vitest@4.1.5) + '@vitest/browser': 4.1.5(bufferutil@4.1.0)(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3))(utf-8-validate@5.0.10)(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1))(vitest@4.1.5) + '@vitest/browser-playwright': 4.1.5(bufferutil@4.1.0)(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3))(playwright@1.59.1)(utf-8-validate@5.0.10)(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1))(vitest@4.1.5) '@vitest/runner': 4.1.5 - vitest: 4.1.5(@types/node@20.19.39)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(jsdom@20.0.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)) + vitest: 4.1.5(@types/node@20.19.39)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(jsdom@20.0.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)) transitivePeerDependencies: - react - react-dom @@ -9447,12 +9655,18 @@ snapshots: dependencies: '@types/node': 20.19.39 + '@types/set-cookie-parser@2.4.10': + dependencies: + '@types/node': 20.19.39 + '@types/sinonjs__fake-timers@8.1.1': {} '@types/sizzle@2.3.10': {} '@types/stack-utils@2.0.3': {} + '@types/statuses@2.0.6': {} + '@types/tmp@0.2.6': {} '@types/tough-cookie@4.0.5': {} @@ -9700,29 +9914,29 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitest/browser-playwright@4.1.5(bufferutil@4.1.0)(playwright@1.59.1)(utf-8-validate@5.0.10)(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1))(vitest@4.1.5)': + '@vitest/browser-playwright@4.1.5(bufferutil@4.1.0)(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3))(playwright@1.59.1)(utf-8-validate@5.0.10)(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1))(vitest@4.1.5)': dependencies: - '@vitest/browser': 4.1.5(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1))(vitest@4.1.5) - '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)) + '@vitest/browser': 4.1.5(bufferutil@4.1.0)(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3))(utf-8-validate@5.0.10)(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1))(vitest@4.1.5) + '@vitest/mocker': 4.1.5(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)) playwright: 1.59.1 tinyrainbow: 3.1.0 - vitest: 4.1.5(@types/node@20.19.39)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(jsdom@20.0.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)) + vitest: 4.1.5(@types/node@20.19.39)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(jsdom@20.0.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser@4.1.5(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1))(vitest@4.1.5)': + '@vitest/browser@4.1.5(bufferutil@4.1.0)(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3))(utf-8-validate@5.0.10)(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1))(vitest@4.1.5)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)) + '@vitest/mocker': 4.1.5(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)) '@vitest/utils': 4.1.5 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.5(@types/node@20.19.39)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(jsdom@20.0.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)) + vitest: 4.1.5(@types/node@20.19.39)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(jsdom@20.0.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)) ws: 8.20.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil @@ -9742,9 +9956,9 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.5(@types/node@20.19.39)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(jsdom@20.0.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)) + vitest: 4.1.5(@types/node@20.19.39)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(jsdom@20.0.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)) optionalDependencies: - '@vitest/browser': 4.1.5(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1))(vitest@4.1.5) + '@vitest/browser': 4.1.5(bufferutil@4.1.0)(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3))(utf-8-validate@5.0.10)(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1))(vitest@4.1.5) '@vitest/expect@3.2.4': dependencies: @@ -9763,12 +9977,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.5(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1))': + '@vitest/mocker@4.1.5(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1))': dependencies: '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: + msw: 2.14.6(@types/node@20.19.39)(typescript@5.9.3) vite: 8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1) '@vitest/pretty-format@3.2.4': @@ -9809,24 +10024,24 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@wagmi/connectors@7.2.1(@coinbase/wallet-sdk@4.3.7(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(@metamask/sdk@0.33.1(bufferutil@4.1.0)(utf-8-validate@5.0.10))(@wagmi/core@3.4.6(@tanstack/query-core@5.100.5)(@types/react@19.2.14)(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.5))(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)))(typescript@5.9.3)(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))': + '@wagmi/connectors@7.2.1(@coinbase/wallet-sdk@4.3.7(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(@metamask/sdk@0.33.1(bufferutil@4.1.0)(utf-8-validate@5.0.10))(@wagmi/core@3.4.0(@tanstack/query-core@5.100.5)(@types/react@19.2.14)(ox@0.14.20(typescript@5.9.3)(zod@4.3.6))(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)))(typescript@5.9.3)(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))': dependencies: - '@wagmi/core': 3.4.6(@tanstack/query-core@5.100.5)(@types/react@19.2.14)(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.5))(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)) + '@wagmi/core': 3.4.0(@tanstack/query-core@5.100.5)(@types/react@19.2.14)(ox@0.14.20(typescript@5.9.3)(zod@4.3.6))(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)) viem: 2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) optionalDependencies: '@coinbase/wallet-sdk': 4.3.7(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) '@metamask/sdk': 0.33.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) typescript: 5.9.3 - '@wagmi/connectors@8.0.5(@coinbase/wallet-sdk@4.3.7(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(@wagmi/core@3.4.6(@tanstack/query-core@5.100.5)(@types/react@19.2.14)(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.5))(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)))(typescript@5.9.3)(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))': + '@wagmi/connectors@8.0.5(@coinbase/wallet-sdk@4.3.7(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(@wagmi/core@3.4.6(@tanstack/query-core@5.100.5)(@types/react@19.2.14)(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)))(typescript@5.9.3)(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))': dependencies: - '@wagmi/core': 3.4.6(@tanstack/query-core@5.100.5)(@types/react@19.2.14)(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.5))(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)) + '@wagmi/core': 3.4.6(@tanstack/query-core@5.100.5)(@types/react@19.2.14)(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)) viem: 2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) optionalDependencies: '@coinbase/wallet-sdk': 4.3.7(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) typescript: 5.9.3 - '@wagmi/core@3.4.6(@tanstack/query-core@5.100.5)(@types/react@19.2.14)(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))': + '@wagmi/core@3.4.0(@tanstack/query-core@5.100.5)(@types/react@19.2.14)(ox@0.14.20(typescript@5.9.3)(zod@4.3.6))(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.9.3) @@ -9834,6 +10049,7 @@ snapshots: zustand: 5.0.0(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.4.0(react@19.2.5)) optionalDependencies: '@tanstack/query-core': 5.100.5 + ox: 0.14.20(typescript@5.9.3)(zod@4.3.6) typescript: 5.9.3 transitivePeerDependencies: - '@types/react' @@ -9841,12 +10057,12 @@ snapshots: - react - use-sync-external-store - '@wagmi/core@3.4.6(@tanstack/query-core@5.100.5)(@types/react@19.2.14)(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.5))(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))': + '@wagmi/core@3.4.6(@tanstack/query-core@5.100.5)(@types/react@19.2.14)(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.9.3) viem: 2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) - zustand: 5.0.0(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)) + zustand: 5.0.0(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.4.0(react@19.2.5)) optionalDependencies: '@tanstack/query-core': 5.100.5 typescript: 5.9.3 @@ -10519,6 +10735,8 @@ snapshots: slice-ansi: 3.0.0 string-width: 4.2.3 + cli-width@4.1.0: {} + client-only@0.0.1: {} cliui@5.0.0: @@ -10588,6 +10806,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie@1.1.1: {} + cookiejar@2.1.4: {} copy-to-clipboard@3.3.3: @@ -11654,6 +11874,16 @@ snapshots: fast-safe-stringify@2.1.1: {} + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -11869,6 +12099,8 @@ snapshots: graceful-fs@4.2.11: {} + graphql@16.14.0: {} + har-schema@2.0.0: {} har-validator@5.1.5: @@ -11915,6 +12147,11 @@ snapshots: dependencies: function-bind: 1.1.2 + headers-polyfill@5.0.1: + dependencies: + '@types/set-cookie-parser': 2.4.10 + set-cookie-parser: 3.1.0 + hermes-estree@0.25.1: {} hermes-parser@0.25.1: @@ -12146,6 +12383,8 @@ snapshots: is-negative-zero@2.0.3: {} + is-node-process@1.2.0: {} + is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -13030,6 +13269,33 @@ snapshots: ms@2.1.3: {} + msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 6.1.0(@types/node@20.19.39) + '@mswjs/interceptors': 0.41.9 + '@open-draft/deferred-promise': 3.0.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.14.0 + headers-polyfill: 5.0.1 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.11.11 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.1 + type-fest: 5.6.0 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + + mute-stream@4.0.0: {} + nanoid@3.3.11: {} napi-postinstall@0.3.4: {} @@ -13181,6 +13447,8 @@ snapshots: ospath@1.2.2: {} + outvariant@1.4.3: {} + own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -13262,6 +13530,8 @@ snapshots: lru-cache: 11.3.5 minipass: 7.1.3 + path-to-regexp@6.3.0: {} + pathe@2.0.3: {} pathval@2.0.1: {} @@ -13705,6 +13975,8 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + rettime@0.11.11: {} + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -13807,6 +14079,8 @@ snapshots: set-blocking@2.0.0: {} + set-cookie-parser@3.1.0: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -13909,6 +14183,8 @@ snapshots: signal-exit@3.0.7: {} + signal-exit@4.1.0: {} + sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.29 @@ -13996,6 +14272,8 @@ snapshots: standard-as-callback@2.1.0: {} + statuses@2.0.2: {} + std-env@4.1.0: {} stop-iteration-iterator@1.1.0: @@ -14025,6 +14303,8 @@ snapshots: - react-dom - utf-8-validate + strict-event-emitter@0.5.1: {} + strict-uri-encode@2.0.0: {} string-length@4.0.2: @@ -14158,6 +14438,8 @@ snapshots: systeminformation@5.31.5: {} + tagged-tag@1.0.0: {} + tailwind-merge@3.5.0: {} tailwindcss@4.2.4: {} @@ -14198,10 +14480,16 @@ snapshots: tldts-core@6.1.86: {} + tldts-core@7.4.0: {} + tldts@6.1.86: dependencies: tldts-core: 6.1.86 + tldts@7.4.0: + dependencies: + tldts-core: 7.4.0 + tmp@0.2.5: {} tmpl@1.0.5: {} @@ -14236,6 +14524,10 @@ snapshots: dependencies: tldts: 6.1.86 + tough-cookie@6.0.1: + dependencies: + tldts: 7.4.0 + tr46@0.0.3: {} tr46@3.0.0: @@ -14291,6 +14583,10 @@ snapshots: type-fest@0.8.1: {} + type-fest@5.6.0: + dependencies: + tagged-tag: 1.0.0 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -14387,6 +14683,8 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + until-async@3.0.2: {} + untildify@4.0.0: {} update-browserslist-db@1.2.3(browserslist@4.28.2): @@ -14546,10 +14844,10 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 - vitest@4.1.5(@types/node@20.19.39)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(jsdom@20.0.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)): + vitest@4.1.5(@types/node@20.19.39)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(jsdom@20.0.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)): dependencies: '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)) + '@vitest/mocker': 4.1.5(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)) '@vitest/pretty-format': 4.1.5 '@vitest/runner': 4.1.5 '@vitest/snapshot': 4.1.5 @@ -14570,7 +14868,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.19.39 - '@vitest/browser-playwright': 4.1.5(bufferutil@4.1.0)(playwright@1.59.1)(utf-8-validate@5.0.10)(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1))(vitest@4.1.5) + '@vitest/browser-playwright': 4.1.5(bufferutil@4.1.0)(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3))(playwright@1.59.1)(utf-8-validate@5.0.10)(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1))(vitest@4.1.5) '@vitest/coverage-v8': 4.1.5(@vitest/browser@4.1.5)(vitest@4.1.5) jsdom: 20.0.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) transitivePeerDependencies: @@ -14585,7 +14883,7 @@ snapshots: wagmi@3.6.5(@coinbase/wallet-sdk@4.3.7(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(@tanstack/query-core@5.100.5)(@tanstack/react-query@5.100.5(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)(typescript@5.9.3)(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)): dependencies: '@tanstack/react-query': 5.100.5(react@19.2.5) - '@wagmi/connectors': 8.0.5(@coinbase/wallet-sdk@4.3.7(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(@wagmi/core@3.4.6(@tanstack/query-core@5.100.5)(@types/react@19.2.14)(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.5))(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)))(typescript@5.9.3)(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)) + '@wagmi/connectors': 8.0.5(@coinbase/wallet-sdk@4.3.7(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(@wagmi/core@3.4.6(@tanstack/query-core@5.100.5)(@types/react@19.2.14)(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)))(typescript@5.9.3)(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)) '@wagmi/core': 3.4.6(@tanstack/query-core@5.100.5)(@types/react@19.2.14)(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)) react: 19.2.5 use-sync-external-store: 1.4.0(react@19.2.5) @@ -14870,14 +15168,8 @@ snapshots: react: 19.2.5 use-sync-external-store: 1.4.0(react@19.2.5) - zustand@5.0.0(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)): - optionalDependencies: - '@types/react': 19.2.14 - react: 19.2.5 - use-sync-external-store: 1.6.0(react@19.2.5) - - zustand@5.0.12(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)): + zustand@5.0.12(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.4.0(react@19.2.5)): optionalDependencies: '@types/react': 19.2.14 react: 19.2.5 - use-sync-external-store: 1.6.0(react@19.2.5) + use-sync-external-store: 1.4.0(react@19.2.5) diff --git a/src/app/api/properties/route.ts b/src/app/api/properties/route.ts index 3cc00707..b56aa1b7 100644 --- a/src/app/api/properties/route.ts +++ b/src/app/api/properties/route.ts @@ -16,7 +16,7 @@ export async function GET(request: NextRequest) { // Parse query parameters const page = parseInt(searchParams.get('page') || '1'); - const resultsPerPage = parseInt(searchParams.get('limit') || '12'); + const resultsPerPage = parseInt(searchParams.get('size') || searchParams.get('limit') || '12'); const sortBy = (searchParams.get('sortBy') || 'newest') as SortOption; const useCache = searchParams.get('cache') !== 'false'; // Default to true diff --git a/src/app/properties/page.tsx b/src/app/properties/page.tsx index f80ad1ef..0d8b9d57 100644 --- a/src/app/properties/page.tsx +++ b/src/app/properties/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { Suspense } from "react"; +import React, { Suspense, useEffect } from "react"; import { SearchFilterForm } from "@/components/forms/SearchFilterForm"; import { SearchResults } from "@/components/SearchResults"; import { WalletConnector } from "@/components/WalletConnector"; @@ -12,15 +12,14 @@ import { useNotificationStore } from "@/store/notificationStore"; import { useWalletStore } from "@/store/walletStore"; import { useNotificationChecker } from "@/hooks/useNotificationChecker"; import { useFavoritesStore } from "@/store/favoritesStore"; +import { usePaginationParams, isValidPageSize, type PageSize } from "@/hooks/usePaginationParams"; import Link from "next/link"; import { Heart } from "lucide-react"; -import { Skeleton } from "@/components/ui/skeleton"; import PropertyPageSkeleton from "@/components/PropertyPageSkeleton"; function PropertiesContent() { const { viewMode: storeViewMode, setViewMode: setStoreViewMode } = useSearchStore(); - const { address } = useWalletStore(); const { alerts, markAsRead, markAllAsRead, clearAlert } = useNotificationStore(); @@ -34,21 +33,50 @@ function PropertiesContent() { const { favorites } = useFavoritesStore(); + // URL-driven pagination params (?page=N&size=N) + const { page: urlPage, size: urlSize, setPage: setUrlPage, setSize: setUrlSize, buildHref } = + usePaginationParams(); + const { filters, sortBy, - page, + page: storePage, + resultsPerPage: storeSize, properties, totalResults, totalPages, isLoading, error, - setFilter, + setFilters, clearFilters, setSortBy, - setPage, + setPage: setStorePage, + setResultsPerPage, } = usePropertySearch(); + // Keep Zustand store in sync with URL params on mount and when URL changes + useEffect(() => { + if (urlPage !== storePage) { + setStorePage(urlPage); + } + }, [urlPage]); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + if (urlSize !== storeSize) { + setResultsPerPage(urlSize); + } + }, [urlSize]); // eslint-disable-line react-hooks/exhaustive-deps + + // Page change: update URL (which triggers the effect above to sync the store) + const handlePageChange = (newPage: number) => { + setUrlPage(newPage); + }; + + // Page size change: update URL (resets to page 1 inside setUrlSize) + const handlePageSizeChange = (newSize: PageSize) => { + setUrlSize(newSize); + }; + return (
{/* Header */} @@ -109,8 +137,15 @@ function PropertiesContent() { { + // Apply full filter object and reset to page 1 + setFilters(newFilters); + setUrlPage(1); + }} + onClearFilters={() => { + clearFilters(); + setUrlPage(1); + }} />
@@ -123,12 +158,18 @@ function PropertiesContent() { error={error} viewMode={viewMode} sortBy={sortBy} - page={page} + page={storePage} totalPages={totalPages} + pageSize={urlSize} filters={filters} onViewModeChange={setViewMode} - onSortChange={setSortBy} - onPageChange={setPage} + onSortChange={(newSort) => { + setSortBy(newSort); + setUrlPage(1); + }} + onPageChange={handlePageChange} + onPageSizeChange={handlePageSizeChange} + buildPageHref={buildHref} /> diff --git a/src/components/PropertyPagination.tsx b/src/components/PropertyPagination.tsx new file mode 100644 index 00000000..351ecfff --- /dev/null +++ b/src/components/PropertyPagination.tsx @@ -0,0 +1,249 @@ +'use client'; + +import React, { useEffect, useCallback } from 'react'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; +import { PAGE_SIZE_OPTIONS, type PageSize } from '@/hooks/usePaginationParams'; + +interface PropertyPaginationProps { + page: number; + totalPages: number; + totalResults: number; + pageSize: PageSize; + onPageChange: (page: number) => void; + onPageSizeChange: (size: PageSize) => void; + /** Optional: pre-built href for each page number (enables native link behaviour) */ + buildHref?: (page: number) => string; +} + +/** + * Full-featured pagination bar: + * - Page size selector (12 / 24 / 48) + * - Total count display + * - Previous / Next buttons + * - Numbered page buttons with ellipsis + * - Keyboard navigation (← →, Home, End) + */ +export const PropertyPagination: React.FC = ({ + page, + totalPages, + totalResults, + pageSize, + onPageChange, + onPageSizeChange, + buildHref, +}) => { + // ── Keyboard navigation ────────────────────────────────────────────────── + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + // Only fire when no input/textarea/select is focused + const tag = (document.activeElement?.tagName ?? '').toLowerCase(); + if (['input', 'textarea', 'select'].includes(tag)) return; + + if (e.key === 'ArrowLeft' && page > 1) { + e.preventDefault(); + onPageChange(page - 1); + } else if (e.key === 'ArrowRight' && page < totalPages) { + e.preventDefault(); + onPageChange(page + 1); + } else if (e.key === 'Home') { + e.preventDefault(); + onPageChange(1); + } else if (e.key === 'End') { + e.preventDefault(); + onPageChange(totalPages); + } + }, + [page, totalPages, onPageChange], + ); + + useEffect(() => { + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [handleKeyDown]); + + // ── Page number window ─────────────────────────────────────────────────── + const getPageNumbers = (): (number | 'ellipsis-start' | 'ellipsis-end')[] => { + if (totalPages <= 7) { + return Array.from({ length: totalPages }, (_, i) => i + 1); + } + + const pages: (number | 'ellipsis-start' | 'ellipsis-end')[] = [1]; + + if (page > 3) pages.push('ellipsis-start'); + + const start = Math.max(2, page - 1); + const end = Math.min(totalPages - 1, page + 1); + + for (let i = start; i <= end; i++) pages.push(i); + + if (page < totalPages - 2) pages.push('ellipsis-end'); + + pages.push(totalPages); + return pages; + }; + + const pageNumbers = getPageNumbers(); + + // ── Range display ──────────────────────────────────────────────────────── + const rangeStart = Math.min((page - 1) * pageSize + 1, totalResults); + const rangeEnd = Math.min(page * pageSize, totalResults); + + if (totalPages <= 1 && totalResults <= pageSize) return null; + + return ( + + ); +}; + +// ── Internal helper ────────────────────────────────────────────────────────── + +interface PageButtonProps extends React.ButtonHTMLAttributes { + href?: string; + active?: boolean; + children: React.ReactNode; +} + +const PageButton: React.FC = ({ + href, + active = false, + disabled = false, + children, + onClick, + ...rest +}) => { + const base = + 'inline-flex items-center justify-center min-w-[2.5rem] h-10 px-2 rounded-lg text-sm font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500'; + const activeClass = 'bg-blue-600 text-white shadow-sm'; + const inactiveClass = + 'border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'; + const disabledClass = 'opacity-40 cursor-not-allowed pointer-events-none'; + + const className = [ + base, + active ? activeClass : inactiveClass, + disabled ? disabledClass : '', + ] + .filter(Boolean) + .join(' '); + + // Use an tag when href is provided so the browser can prefetch / open in new tab + if (href && !disabled) { + return ( + { + e.preventDefault(); + onClick?.(e as unknown as React.MouseEvent); + }} + className={className} + {...(rest as React.AnchorHTMLAttributes)} + > + {children} + + ); + } + + return ( + + ); +}; diff --git a/src/components/SearchResults.tsx b/src/components/SearchResults.tsx index 2e16a82f..aded03c7 100644 --- a/src/components/SearchResults.tsx +++ b/src/components/SearchResults.tsx @@ -4,12 +4,14 @@ import React, { useRef, useEffect } from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; import { PropertyCard } from './PropertyCard'; import { SaveSearchButton } from './SaveSearchButton'; +import { PropertyPagination } from './PropertyPagination'; import type { Property, ViewMode, SortOption, SearchFilters } from '@/types/property'; import { SORT_LABELS } from '@/types/property'; import { Skeleton } from '@/components/ui/skeleton'; import { ComparisonBar } from './ComparisonBar'; import { EmptyState } from '@/components/ui/EmptyState'; import { Search } from 'lucide-react'; +import type { PageSize } from '@/hooks/usePaginationParams'; interface SearchResultsProps { properties: Property[]; @@ -20,10 +22,14 @@ interface SearchResultsProps { sortBy: SortOption; page: number; totalPages: number; + pageSize: PageSize; filters: SearchFilters; onViewModeChange: (mode: 'grid' | 'list') => void; onSortChange: (sort: SortOption) => void; onPageChange: (page: number) => void; + onPageSizeChange: (size: PageSize) => void; + /** Optional href builder for accessible anchor-based page links */ + buildPageHref?: (page: number) => string; } export const SearchResults: React.FC = ({ @@ -35,10 +41,13 @@ export const SearchResults: React.FC = ({ sortBy, page, totalPages, + pageSize, filters, onViewModeChange, onSortChange, onPageChange, + onPageSizeChange, + buildPageHref, }) => { const parentRef = useRef(null); @@ -89,9 +98,9 @@ export const SearchResults: React.FC = ({

- {isLoading ? 'Searching...' : `${totalResults} Properties Found`} + {isLoading ? 'Searching...' : `${totalResults.toLocaleString()} Properties Found`}

- {page > 1 && ( + {totalPages > 1 && (

Page {page} of {totalPages}

@@ -195,49 +204,15 @@ export const SearchResults: React.FC = ({
{/* Pagination */} - {totalPages > 1 && ( -
- - -
- {[...Array(Math.min(totalPages, 5))].map((_, i) => { - let pageNum; - if (totalPages <= 5) pageNum = i + 1; - else if (page <= 3) pageNum = i + 1; - else if (page >= totalPages - 2) pageNum = totalPages - 4 + i; - else pageNum = page - 2 + i; - - return ( - - ); - })} -
- - -
- )} + )}
diff --git a/src/hooks/usePaginationParams.ts b/src/hooks/usePaginationParams.ts new file mode 100644 index 00000000..b2141cb5 --- /dev/null +++ b/src/hooks/usePaginationParams.ts @@ -0,0 +1,67 @@ +'use client'; + +import { useCallback } from 'react'; +import { useRouter, useSearchParams, usePathname } from 'next/navigation'; + +export const PAGE_SIZE_OPTIONS = [12, 24, 48] as const; +export type PageSize = (typeof PAGE_SIZE_OPTIONS)[number]; + +export function isValidPageSize(value: number): value is PageSize { + return PAGE_SIZE_OPTIONS.includes(value as PageSize); +} + +export interface PaginationParams { + page: number; + size: PageSize; +} + +/** + * Reads ?page and ?size from the URL and provides setters that update the URL. + * Keeps the Zustand store in sync so the React Query hook picks up the changes. + */ +export function usePaginationParams(): PaginationParams & { + setPage: (page: number) => void; + setSize: (size: PageSize) => void; + buildHref: (page: number, size?: PageSize) => string; +} { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + const rawPage = parseInt(searchParams.get('page') ?? '1', 10); + const rawSize = parseInt(searchParams.get('size') ?? '12', 10); + + const page = Number.isFinite(rawPage) && rawPage >= 1 ? rawPage : 1; + const size: PageSize = isValidPageSize(rawSize) ? rawSize : 12; + + /** Build a URL string with updated pagination params, preserving other params */ + const buildHref = useCallback( + (newPage: number, newSize: PageSize = size) => { + const params = new URLSearchParams(searchParams.toString()); + params.set('page', String(newPage)); + params.set('size', String(newSize)); + return `${pathname}?${params.toString()}`; + }, + [pathname, searchParams, size], + ); + + const setPage = useCallback( + (newPage: number) => { + router.push(buildHref(newPage), { scroll: false }); + // Scroll to top of results smoothly + window.scrollTo({ top: 0, behavior: 'smooth' }); + }, + [router, buildHref], + ); + + const setSize = useCallback( + (newSize: PageSize) => { + // Reset to page 1 when page size changes + router.push(buildHref(1, newSize), { scroll: false }); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }, + [router, buildHref], + ); + + return { page, size, setPage, setSize, buildHref }; +} diff --git a/src/hooks/usePropertySearchQuery.ts b/src/hooks/usePropertySearchQuery.ts index d03d976f..e1513dbf 100644 --- a/src/hooks/usePropertySearchQuery.ts +++ b/src/hooks/usePropertySearchQuery.ts @@ -104,6 +104,7 @@ export function usePropertySearch() { clearFilters, setSortBy, setPage, + setResultsPerPage, } = searchStore; const query = usePropertySearchQuery(filters, sortBy, page, resultsPerPage); @@ -133,6 +134,10 @@ export function usePropertySearch() { } }; + const handleResultsPerPageChange = (count: number) => { + setResultsPerPage(count); + }; + const totalPages = query.data ? Math.ceil(query.data.total / resultsPerPage) : 0; return { @@ -149,10 +154,12 @@ export function usePropertySearch() { lastUpdated: query.dataUpdatedAt ? new Date(query.dataUpdatedAt) : undefined, // Actions + setFilters, setFilter: handleFilterChange, clearFilters: handleClearFilters, setSortBy: handleSortChange, setPage: (newPage: number) => handlePageChange(newPage), + setResultsPerPage: handleResultsPerPageChange, loadMore: () => handlePageChange(page + 1, { scrollToTop: false }), refetch: query.refetch, }; diff --git a/src/lib/propertyService.ts b/src/lib/propertyService.ts index e0812a3a..805aef94 100644 --- a/src/lib/propertyService.ts +++ b/src/lib/propertyService.ts @@ -105,6 +105,7 @@ class PropertyService { /** * Fetch search results from network and cache them + * Implements server-side pagination: only returns the requested page */ private async fetchAndCacheSearch( filters: SearchFilters, @@ -115,32 +116,35 @@ class PropertyService { // Simulate API delay await this.delay(300); - let results = [...MOCK_PROPERTIES]; + // Apply filters to all data (in a real DB, this would be WHERE clause) + let results = this.applyFilters([...MOCK_PROPERTIES], filters); - // Apply filters - results = this.applyFilters(results, filters); - - // Apply sorting + // Apply sorting (in a real DB, this would be ORDER BY) results = this.applySorting(results, sortBy); - // Calculate pagination + // Server-side pagination: calculate total and slice before returning const total = results.length; const totalPages = Math.ceil(total / resultsPerPage); - const startIndex = (page - 1) * resultsPerPage; + + // Validate page number + const validPage = Math.max(1, Math.min(page, totalPages || 1)); + const startIndex = (validPage - 1) * resultsPerPage; const endIndex = startIndex + resultsPerPage; + + // Only return the requested page of data (server-side pagination) const paginatedResults = results.slice(startIndex, endIndex); const result: PropertySearchResult = { properties: paginatedResults, total, - page, + page: validPage, totalPages, }; // Cache the result in both Redis and local cache try { // Cache in Redis first (primary cache) - await redisCacheService.setPropertyListings(filters, sortBy, page, result); + await redisCacheService.setPropertyListings(filters, sortBy, validPage, result); // Also cache in local cache as fallback await cacheSearchResult(filters, sortBy, result);