diff --git a/package.json b/package.json index 9688f0f..45416c7 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "motion": "^12.5.0", "next": "15.2.1", "next-themes": "^0.4.6", + "qrcode": "^1.5.4", "react": "^19.0.0", "react-day-picker": "8.10.1", "react-dom": "^19.0.0", @@ -44,6 +45,7 @@ "@stylistic/eslint-plugin": "^4.2.0", "@tailwindcss/postcss": "^4", "@types/node": "^20", + "@types/qrcode": "^1.5.5", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6bd0f56..7b3c8a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + qrcode: + specifier: ^1.5.4 + version: 1.5.4 react: specifier: ^19.0.0 version: 19.0.0 @@ -108,6 +111,9 @@ importers: '@types/node': specifier: ^20 version: 20.17.23 + '@types/qrcode': + specifier: ^1.5.5 + version: 1.5.5 '@types/react': specifier: ^19 version: 19.0.10 @@ -1084,6 +1090,9 @@ packages: '@types/node@20.17.23': resolution: {integrity: sha512-8PCGZ1ZJbEZuYNTMqywO+Sj4vSKjSjT6Ua+6RFOYlEvIvKQABPtrNkoVSLSKDb4obYcMhspVKmsw8Cm10NFRUg==} + '@types/qrcode@1.5.5': + resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==} + '@types/react-dom@19.0.4': resolution: {integrity: sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==} peerDependencies: @@ -1152,6 +1161,10 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -1260,6 +1273,10 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + caniuse-lite@1.0.30001701: resolution: {integrity: sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw==} @@ -1280,6 +1297,9 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1349,6 +1369,10 @@ packages: supports-color: optional: true + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -1380,6 +1404,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -1388,6 +1415,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} @@ -1590,6 +1620,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1632,6 +1666,10 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -1778,6 +1816,10 @@ packages: resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} engines: {node: '>= 0.4'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-generator-function@1.1.0: resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} engines: {node: '>= 0.4'} @@ -1954,6 +1996,10 @@ packages: resolution: {integrity: sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==} engines: {node: '>= 12.0.0'} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -2110,14 +2156,26 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -2144,6 +2202,10 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -2175,6 +2237,11 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -2248,6 +2315,13 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2298,6 +2372,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -2364,6 +2441,10 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + string.prototype.includes@2.0.1: resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} engines: {node: '>= 0.4'} @@ -2390,6 +2471,10 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -2538,6 +2623,9 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which-typed-array@1.1.18: resolution: {integrity: sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==} engines: {node: '>= 0.4'} @@ -2551,9 +2639,24 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -3375,6 +3478,10 @@ snapshots: dependencies: undici-types: 6.19.8 + '@types/qrcode@1.5.5': + dependencies: + '@types/node': 20.17.23 + '@types/react-dom@19.0.4(@types/react@19.0.10)': dependencies: '@types/react': 19.0.10 @@ -3473,6 +3580,8 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-regex@5.0.1: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -3612,6 +3721,8 @@ snapshots: callsites@3.1.0: {} + camelcase@5.3.1: {} + caniuse-lite@1.0.30001701: {} canvas@3.1.0: @@ -3632,6 +3743,12 @@ snapshots: client-only@0.0.1: {} + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + clsx@2.1.1: {} cmdk@1.1.1(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): @@ -3704,6 +3821,8 @@ snapshots: dependencies: ms: 2.1.3 + decamelize@1.2.0: {} + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -3730,6 +3849,8 @@ snapshots: detect-node-es@1.1.0: {} + dijkstrajs@1.0.3: {} + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -3740,6 +3861,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} end-of-stream@1.4.4: @@ -4087,6 +4210,11 @@ snapshots: dependencies: to-regex-range: 5.0.1 + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -4127,6 +4255,8 @@ snapshots: functions-have-names@1.2.3: {} + get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4276,6 +4406,8 @@ snapshots: dependencies: call-bound: 1.0.4 + is-fullwidth-code-point@3.0.0: {} + is-generator-function@1.1.0: dependencies: call-bound: 1.0.4 @@ -4433,6 +4565,10 @@ snapshots: lightningcss-win32-arm64-msvc: 1.29.1 lightningcss-win32-x64-msvc: 1.29.1 + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -4588,14 +4724,24 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 + p-try@2.2.0: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -4612,6 +4758,8 @@ snapshots: picomatch@4.0.2: {} + pngjs@5.0.0: {} + possible-typed-array-names@1.1.0: {} postcss@8.4.31: @@ -4656,6 +4804,12 @@ snapshots: punycode@2.3.1: {} + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + queue-microtask@1.2.3: {} rc@1.2.8: @@ -4736,6 +4890,10 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + require-directory@2.1.1: {} + + require-main-filename@2.0.0: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -4785,6 +4943,8 @@ snapshots: semver@7.7.1: {} + set-blocking@2.0.0: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -4892,6 +5052,12 @@ snapshots: streamsearch@1.1.0: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + string.prototype.includes@2.0.1: dependencies: call-bind: 1.0.8 @@ -4946,6 +5112,10 @@ snapshots: dependencies: safe-buffer: 5.2.1 + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + strip-bom@3.0.0: {} strip-json-comments@2.0.1: {} @@ -5114,6 +5284,8 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 + which-module@2.0.1: {} + which-typed-array@1.1.18: dependencies: available-typed-arrays: 1.0.7 @@ -5129,8 +5301,35 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrappy@1.0.2: {} + y18n@4.0.3: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + yocto-queue@0.1.0: {} zod@3.24.2: {} diff --git a/src/actions/auth/identify.ts b/src/actions/auth/identify.ts new file mode 100644 index 0000000..9400e38 --- /dev/null +++ b/src/actions/auth/identify.ts @@ -0,0 +1,21 @@ +import {callByUser, callPublic} from '@/actions/base' + +export async function Identify(props: { + type: number + name: string + iden_no: string +}) { + return await callByUser<{ + identified: boolean + target: string + }>('/api/user/identify', props) +} + +export async function IdentifyCallback(props: { + id: string +}) { + return await callPublic<{ + success: boolean + message: string + }>('/api/user/identify/callback', props) +} diff --git a/src/actions/base.ts b/src/actions/base.ts index 4a96400..1e939e7 100644 --- a/src/actions/base.ts +++ b/src/actions/base.ts @@ -238,6 +238,48 @@ async function callByUser( // endregion +// ====================== +// region no token +// ====================== + +// 不需要令牌的公共API调用函数 +async function callPublic( + endpoint: string, + data?: unknown, +): Promise> { + try { + // 发送请求 + const requestOptions: RequestInit = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: data ? JSON.stringify(data) : undefined, + } + + const response = await fetch(`${API_BASE_URL}${endpoint}`, requestOptions) + + // 检查响应状态 + if (!response.ok) { + console.log('公共接口响应不成功', `status=${response.status}`, await response.text()) + + return { + status: response.status, + success: false, + message: response.status >= 500 ? '服务器错误' : '请求失败', + } + } + + return handleResponse(response) + } + catch (e) { + console.error('Public API call failed:', e) + throw new Error('服务调用失败', { cause: e }) + } +} + +// endregion + // 统一响应解析 async function handleResponse(response: Response): Promise> { @@ -286,4 +328,5 @@ export { getUserToken, callByDevice, callByUser, + callPublic, } diff --git a/src/app/(api)/identify/callback/page.tsx b/src/app/(api)/identify/callback/page.tsx new file mode 100644 index 0000000..f094981 --- /dev/null +++ b/src/app/(api)/identify/callback/page.tsx @@ -0,0 +1,76 @@ +'use client' +import {useEffect, useState} from 'react' +import {useSearchParams} from 'next/navigation' +import {IdentifyCallback} from '@/actions/auth/identify' +import {Card, CardContent} from '@/components/ui/card' +import {CheckCircle, AlertCircle, Loader2} from 'lucide-react' + +export type PageProps = {} + +export default function Page(props: PageProps) { + const params = useSearchParams() + const success = params.get('success') === 'true' + const id = params.get('id') || '' + + const [result, setResult] = useState({ + status: 'load', + message: '处理中', + }) + + useEffect(() => { + if (success) { + IdentifyCallback({id}).then(resp => { + if (!resp.success) { + setResult({ + status: 'fail', + message: resp.message || '获取活体检测结果失败', + }) + } + else { + const data = resp.data + setResult({ + status: data.success ? 'done' : 'fail', + message: data.message || data.success + ? '认证已完成' + : '认证失败', + }) + } + }) + } + else { + setResult({ + status: 'fail', + message: '未完成认证', + }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( +
+ + + {result.status === 'load' ? (<> + +

{result.message}

+

+ 请保持网络畅通 +

+ ) : result.status === 'done' ? (<> + +

{result.message}

+

+ 认证已完成,您现在可以关闭此页面 +

+ ) : (<> + +

{result.message}

+

+ 认证失败,请重新发起认证 +

+ )} +
+
+
+ ) +} diff --git a/src/app/admin/identify/page.tsx b/src/app/admin/identify/page.tsx index b6a9907..1550c32 100644 --- a/src/app/admin/identify/page.tsx +++ b/src/app/admin/identify/page.tsx @@ -1,12 +1,102 @@ +'use client' import {Button} from '@/components/ui/button' import banner from './_assets/banner.webp' import personal from './_assets/personal.webp' import Image from 'next/image' import Page from '@/components/page' +import {Dialog, DialogContent, DialogFooter, DialogTitle, DialogTrigger} from '@/components/ui/dialog' +import {Form, FormField} from '@/components/ui/form' +import {useForm} from 'react-hook-form' +import zod from 'zod' +import {zodResolver} from '@hookform/resolvers/zod' +import {Identify} from '@/actions/auth/identify' +import {toast} from 'sonner' +import {useContext, useEffect, useRef, useState} from 'react' +import * as qrcode from 'qrcode' +import {AuthContext} from '@/components/providers/AuthProvider' export type IdentifyPageProps = {} -export default async function IdentifyPage(props: IdentifyPageProps) { +export default function IdentifyPage(props: IdentifyPageProps) { + + // ====================== + // 填写信息 + // ====================== + + const schema = zod.object({ + type: zod.enum([`personal`, `enterprise`], {errorMap: () => ({message: `请选择认证类型`})}).default('personal'), + name: zod.string().min(2, {message: `姓名至少2个字符`}), + iden_no: zod.string().length(18, {message: `身份证号码必须为18位`}), + }) + type Schema = zod.infer + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + type: `personal`, + name: ``, + iden_no: ``, + }, + }) + + const handler = form.handleSubmit( + async (value) => { + const resp = await Identify({ + type: value.type === `personal` ? 1 : 2, + name: value.name, + iden_no: value.iden_no, + }) + if (resp.success) { + form.reset() + if (!resp.data?.identified) { + setStep('scan') + setTarget(resp.data.target) + } + else { + toast.success('认证已完成') + } + } + else { + console.log(resp.message) + toast.error(`认证信息提交失败:请稍后重试`) + } + }, + ) + + // ====================== + // 扫码认证 + // ====================== + + const [step, setStep] = useState<'form' | 'scan'>('form') + const [target, setTarget] = useState('') + const [openDialog, setOpenDialog] = useState(false) + const canvas = useRef(null) + + useEffect(() => { + if (canvas.current && target) { + qrcode.toCanvas(canvas.current, target, { + width: 256, + margin: 0, + }, (error) => { + if (error) { + console.error(error) + toast.error(`二维码生成失败:请稍后重试`) + } + }) + } + }, [target]) + + // ====================== + // 用户数据 + // ====================== + + const ctx = useContext(AuthContext) + console.log('render identify page') + + // ====================== + // render + // ====================== + return (
@@ -26,7 +116,62 @@ export default async function IdentifyPage(props: IdentifyPageProps) {

个人认证

平台授权支付宝安全认证,不会泄露您的认证信息

- + {ctx.profile?.id_token ? ( +
+

已完成实名认证

+
+ ) : ( + + + + + + + {step === 'form' ? `实名认证` : `扫码完成认证`} + + {step === 'form' && ( +
+ name={`name`} label={`姓名`}> + {({id, field}) => ( + + )} + + name={`iden_no`} label={`身份证号`}> + {({id, field}) => ( + + )} + + + + + + )} + {step === 'scan' && ( +
+ +

请扫码完成认证

+ +
+ )} +
+
+ )} diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 1c591b1..7b8b118 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -32,16 +32,16 @@ export default async function DashboardLayout(props: DashboardLayoutProps) {
{/* logo */}
- {`logo`} + {`logo`}
{/* title */} -
+
欢迎来到,蓝狐代理
{/* profile */} -
+
diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx index efde24e..ff4ba11 100644 --- a/src/components/ui/form.tsx +++ b/src/components/ui/form.tsx @@ -14,25 +14,31 @@ import { import {merge} from '@/lib/utils' import {Label} from '@/components/ui/label' -import {BaseSyntheticEvent, ComponentProps, createContext, ReactNode, useContext, useId} from 'react' +import React, {BaseSyntheticEvent, ComponentProps, createContext, ReactNode, useContext, useId} from 'react' type FormProps = { form: UseFormReturn - onSubmit: SubmitHandler + onSubmit?: SubmitHandler onError?: SubmitErrorHandler + handler?: (e?: React.BaseSyntheticEvent) => Promise } & Omit, 'onSubmit' | 'onError'> function Form(rawProps: FormProps) { - const {children, onSubmit, onError, ...props} = rawProps + const {children, onSubmit, onError, handler, ...props} = rawProps const form = props.form + const handle = handler || form.handleSubmit( + onSubmit || (_ => {}), + onError, + ) + return ( -
{ + { event.preventDefault() - form.handleSubmit(onSubmit, onError)(event) event.stopPropagation() + await handle(event) }}> {children}