diff --git a/package.json b/package.json index 533bee9..177391d 100644 --- a/package.json +++ b/package.json @@ -17,18 +17,24 @@ "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", "@tailwindcss/vite": "^4.1.18", + "axios": "^1.13.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "crypto-js": "^4.2.0", "lucide-react": "^0.563.0", + "next-themes": "^0.4.6", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router-dom": "^7.13.0", + "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", + "uuid": "^13.0.0", "zustand": "^5.0.10" }, "devDependencies": { "@eslint/js": "^9.39.1", + "@types/crypto-js": "^4.2.2", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a83e03..0c2f8fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,15 +29,24 @@ importers: '@tailwindcss/vite': specifier: ^4.1.18 version: 4.1.18(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)) + axios: + specifier: ^1.13.4 + version: 1.13.4 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 clsx: specifier: ^2.1.1 version: 2.1.1 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 lucide-react: specifier: ^0.563.0 version: 0.563.0(react@19.2.4) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: ^19.2.0 version: 19.2.4 @@ -47,12 +56,18 @@ importers: react-router-dom: specifier: ^7.13.0 version: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tailwind-merge: specifier: ^3.4.0 version: 3.4.0 tailwindcss: specifier: ^4.1.18 version: 4.1.18 + uuid: + specifier: ^13.0.0 + version: 13.0.0 zustand: specifier: ^5.0.10 version: 5.0.10(@types/react@19.2.10)(react@19.2.4) @@ -60,6 +75,9 @@ importers: '@eslint/js': specifier: ^9.39.1 version: 9.39.2 + '@types/crypto-js': + specifier: ^4.2.2 + version: 4.2.2 '@types/node': specifier: ^24.10.1 version: 24.10.9 @@ -1022,6 +1040,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/crypto-js@4.2.2': + resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1128,6 +1149,12 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.13.4: + resolution: {integrity: sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==} + babel-plugin-react-compiler@1.0.0: resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} @@ -1149,6 +1176,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1174,6 +1205,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1188,6 +1223,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -1203,6 +1241,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -1210,6 +1252,10 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + electron-to-chromium@1.5.283: resolution: {integrity: sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==} @@ -1217,6 +1263,22 @@ packages: resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} engines: {node: '>=10.13.0'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.27.2: resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} engines: {node: '>=18'} @@ -1316,19 +1378,43 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -1341,6 +1427,10 @@ packages: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -1348,6 +1438,18 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} @@ -1510,6 +1612,18 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1528,6 +1642,12 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -1570,6 +1690,9 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1666,6 +1789,12 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1752,6 +1881,10 @@ packages: '@types/react': optional: true + uuid@13.0.0: + resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + hasBin: true + vite@7.3.1: resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2598,6 +2731,8 @@ snapshots: dependencies: '@babel/types': 7.28.6 + '@types/crypto-js@4.2.2': {} + '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} @@ -2740,6 +2875,16 @@ snapshots: dependencies: tslib: 2.8.1 + asynckit@0.4.0: {} + + axios@1.13.4: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + babel-plugin-react-compiler@1.0.0: dependencies: '@babel/types': 7.28.6 @@ -2765,6 +2910,11 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + callsites@3.1.0: {} caniuse-lite@1.0.30001766: {} @@ -2786,6 +2936,10 @@ snapshots: color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + concat-map@0.0.1: {} convert-source-map@2.0.0: {} @@ -2798,6 +2952,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-js@4.2.0: {} + csstype@3.2.3: {} debug@4.4.3: @@ -2806,10 +2962,18 @@ snapshots: deep-is@0.1.4: {} + delayed-stream@1.0.0: {} + detect-libc@2.1.2: {} detect-node-es@1.1.0: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + electron-to-chromium@1.5.283: {} enhanced-resolve@5.18.4: @@ -2817,6 +2981,21 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.0 + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild@0.27.2: optionalDependencies: '@esbuild/aix-ppc64': 0.27.2 @@ -2959,13 +3138,43 @@ snapshots: flatted@3.3.3: {} + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + gensync@1.0.0-beta.2: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -2974,10 +3183,22 @@ snapshots: globals@16.5.0: {} + gopd@1.2.0: {} + graceful-fs@4.2.11: {} has-flag@4.0.0: {} + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + hermes-estree@0.25.1: {} hermes-parser@0.25.1: @@ -3097,6 +3318,14 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -3111,6 +3340,11 @@ snapshots: natural-compare@1.4.0: {} + next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + node-releases@2.0.27: {} optionator@0.9.4: @@ -3150,6 +3384,8 @@ snapshots: prelude-ls@1.2.1: {} + proxy-from-env@1.1.0: {} + punycode@2.3.1: {} react-dom@19.2.4(react@19.2.4): @@ -3249,6 +3485,11 @@ snapshots: shebang-regex@3.0.0: {} + sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + source-map-js@1.2.1: {} strip-json-comments@3.1.1: {} @@ -3320,6 +3561,8 @@ snapshots: optionalDependencies: '@types/react': 19.2.10 + uuid@13.0.0: {} + vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2): dependencies: esbuild: 0.27.2 diff --git a/src/App.tsx b/src/App.tsx index e9d29b6..499a19b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,11 @@ import { RouterProvider } from "react-router-dom"; import router from "./routes.tsx"; import "./App.module.css"; +import useUserStore from '@/store/useUserStore'; function App() { + useUserStore() + // console.log(useUserStore()); + return ( ); diff --git a/src/Layout.tsx b/src/Layout.tsx index 610fa1a..671a2ba 100644 --- a/src/Layout.tsx +++ b/src/Layout.tsx @@ -1,11 +1,13 @@ import Header from "./components/Header.tsx"; import { Outlet } from "react-router-dom"; +import { Toaster } from "@/components/ui/sonner" function Layout() { return ( <>
+
); diff --git a/src/api/user.ts b/src/api/user.ts new file mode 100644 index 0000000..5034452 --- /dev/null +++ b/src/api/user.ts @@ -0,0 +1,18 @@ +import http from '@/lib/http' +import type { Result } from '@/types/common' +import type { UserRegisterT } from '@/types/user' +import CryptoJS from 'crypto-js'; + +const prefix = '/user' +export const getCaptcha = (reqId:string): Promise>=>{ + return http.get(prefix+'/captcha/'+reqId) +} + +export const register = (user: UserRegisterT):Promise>=>{ + user.password = CryptoJS.MD5(user.password as string).toString() + return http.post(prefix+'/register',user) +} + +export const existUsername = (username:string):Promise>=>{ + return http.get(prefix+'/exist/'+username) +} \ No newline at end of file diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 0000000..9f46e06 --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,38 @@ +import { + CircleCheckIcon, + InfoIcon, + Loader2Icon, + OctagonXIcon, + TriangleAlertIcon, +} from "lucide-react" +import { useTheme } from "next-themes" +import { Toaster as Sonner, type ToasterProps } from "sonner" + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + , + info: , + warning: , + error: , + loading: , + }} + style={ + { + "--normal-bg": "var(--popover)", + "--normal-text": "var(--popover-foreground)", + "--normal-border": "var(--border)", + "--border-radius": "var(--radius)", + } as React.CSSProperties + } + {...props} + /> + ) +} + +export { Toaster } diff --git a/src/lib/http.ts b/src/lib/http.ts new file mode 100644 index 0000000..efdc2cb --- /dev/null +++ b/src/lib/http.ts @@ -0,0 +1,17 @@ +import axios from 'axios'; + +const request = axios.create({ + baseURL: '/api', // 指向你的 Java SpringBoot 后端地址 + timeout: 10000, +}); + +// 响应拦截器 +request.interceptors.response.use( + (response) => response.data, + (error) => { + console.error('网络请求出错:', error); + return Promise.reject(error); + } +); + +export default request; \ No newline at end of file diff --git a/src/lib/keyGen.ts b/src/lib/keyGen.ts new file mode 100644 index 0000000..d5a030b --- /dev/null +++ b/src/lib/keyGen.ts @@ -0,0 +1,4 @@ +import { v4 as uuidv4 } from 'uuid'; +export const uuid = ()=>{ + return uuidv4() +} \ No newline at end of file diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index c499657..b444ec7 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -1,7 +1,238 @@ import { Input } from "@/components/ui/input"; -import { Checkbox } from "@/components/ui/checkbox" +import { Checkbox } from "@/components/ui/checkbox"; import { Button } from "@/components/ui/button"; +import { useState, useEffect, useRef } from "react"; +import { existUsername, getCaptcha, register } from "@/api/user"; +import useUserStore from "@/store/useUserStore"; +import type { UserRegisterT } from "@/types/user"; +import { toast } from "sonner" +interface UserRegisterValidT { + username: boolean; + passwd: boolean; + email: boolean; + inviteCode: boolean; + agree: boolean; +} + +interface UsernameErrorT { + min: string; + tobe: string; +} + +interface PasswdErrorT { + min: string; +} + +interface SepasswdErrorT { + once: string; +} + +interface EmailErrorT { + pattern: string; +} +interface InviteCodeErrorT { + no: string; + invalid: string; +} +const inviteCodeError: InviteCodeErrorT = { + no: "邀请码不能为空", + invalid: "无效的邀请码", +}; + +const emailError: EmailErrorT = { + pattern: "邮箱格式不正确", +}; +const passwdError: PasswdErrorT = { + min: "密码必须大于6位", +}; + +const usernameError: UsernameErrorT = { + min: "用户名不能少于4位", + tobe: "用户名已存在", +}; + +const sepasswdError: SepasswdErrorT = { + once: "两次输入的密码不一致", +}; + function Register() { + const [userRegister, setUserRegister] = useState({}); + const [usernameErrorMsg, setUsernameErrorMsg] = useState(""); + const [passwdErrorMsg, setPasswdErrorMsg] = useState(""); + const [sepasswdErrorMsg, setSepasswdErrorMsg] = useState(""); + const [emailErrorMsg, setEmailErrorMsg] = useState(""); + const [inviteCodeErrorMsg, setInviteCodeErrorMsg] = useState(""); + const [captcha, setCaptcha] = useState(null); + const captchaId = useUserStore((state) => state.key); + const [userRegisterValid, setUserRegisterValid] = + useState({ + username: false, + passwd: false, + email: false, + inviteCode: false, + agree: false, + }); + + // 注册事件 + const registerEvent = async () => { + let valid = true; + Object.entries(userRegisterValid).forEach(([, value]) => { + if (!value) { + valid = false; + } + }); + if(!userRegisterValid.agree){ + toast.warning('请同意用户及隐私协议',{ position: "top-center" }) + } + if (valid) { + userRegister.key = captchaId + const res = await register(userRegister) + if(res.code!=0){ + toast.error('注册成功',{ position: "top-center" }) + }else{ + toast.info(res.msg,{ position: "top-center" }) + } + } + }; + + // 刷新验证码 + // 这里的类型会自动推断为 number + const timerRef = useRef | null>(null); + const refreshCaptcha = () => { + // 1. 先清除上一个还没执行的定时器(真正的防抖核心) + if (timerRef.current) { + clearTimeout(timerRef.current); + } + + // 2. 开启新定时器,并存入 ref + timerRef.current = setTimeout(async () => { + try { + const res = await getCaptcha(captchaId); + if (res.code === 0) { + setCaptcha(res.data); + } + } finally { + timerRef.current = null; // 执行完清空引用 + } + }, 500); + }; + + // 初始化验证码 + useEffect(() => { + const fetchCaptcha = async () => { + const res = await getCaptcha(captchaId); + if (res.code == 0) { + setCaptcha(res.data); + } + }; + fetchCaptcha(); + }, []); + function inviteCodeChangeEvent(inviteCode: string) { + inviteCodeValidator(inviteCode); + } + + // 邀请码检查 + function inviteCodeValidator(inviteCode: string) { + if (!inviteCode) { + setInviteCodeErrorMsg(inviteCodeError.no); + setUserRegisterValid({ ...userRegisterValid, inviteCode: false }); + } else { + setInviteCodeErrorMsg(""); + setUserRegisterValid({ ...userRegisterValid, inviteCode: true }); + setUserRegister({...userRegister, inviteCode}) + } + } + + // 邮箱变更事件 + function emailChangeEvent(email: string) { + emailValidator(email); + } + + // 邮箱检查 + function emailValidator(email: string) { + const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + if (!emailRegex.test(email)) { + setEmailErrorMsg(emailError.pattern); + setUserRegisterValid({ ...userRegisterValid, email: false }); + } else { + setEmailErrorMsg(""); + setUserRegisterValid({ ...userRegisterValid, email: true }); + setUserRegister({...userRegister, email}) + } + } + // 再次输入密码变更事件 + function sepasswdChangeEvent(passwd: string) { + sepasswdValidator(passwd); + } + + // 再次输入密码检查 + function sepasswdValidator(passwd: string) { + if (userRegister.password != passwd) { + setSepasswdErrorMsg(sepasswdError.once); + } else { + setSepasswdErrorMsg(""); + } + } + + // 密码变更事件 + function passwdChangeEvent(passwd: string) { + passwdValidator(passwd); + setUserRegister({ ...userRegister, password: passwd }); + } + + // 密码检查 + function passwdValidator(username: string) { + if (username.length < 6) { + setPasswdErrorMsg(passwdError.min); + setUserRegisterValid({ ...userRegisterValid, passwd: false }); + } else { + setPasswdErrorMsg(""); + setUserRegisterValid({ ...userRegisterValid, passwd: true }); + } + } + + // 用户名变更事件 + function usernameChangeEvent(username: string) { + usernameValidator(username); + setUserRegister({ ...userRegister, username }); + } + + // 用户名检查 + function usernameValidator(username: string) { + if (username.length < 4) { + setUsernameErrorMsg(usernameError.min); + setUserRegisterValid({ ...userRegisterValid, username: false }); + } else { + setUsernameErrorMsg(""); + setUserRegisterValid({ ...userRegisterValid, username: true }); + } + uniqueUsername(username); + } + + const agreeTimerRef = useRef | null>(null); + // 验证用户名是否唯一 + const uniqueUsername = (username: string) => { + if (agreeTimerRef.current) { + clearTimeout(agreeTimerRef.current); + } + agreeTimerRef.current = setTimeout(async () => { + try { + const res = await existUsername(username); + if (res.code == 0) { + setUsernameErrorMsg(usernameError.tobe); + setUserRegisterValid({ ...userRegisterValid, username: false }); + } + } finally { + agreeTimerRef.current = null; + } + },1000); + }; + + // 验证码变更事件 + const verificationCodeEvent = (code: string)=>{ + setUserRegister({...userRegister, verificationCode:code}) + } + return (
@@ -11,56 +242,159 @@ function Register() {
- - - -
-
用户名已被注册
+ usernameChangeEvent(e.target.value)} + id="username" + placeholder="请输入用户名" + className="mb-1 flex-3" + /> + +
+ {usernameErrorMsg != "" ? ( +
+ {usernameErrorMsg} +
+ ) : ( +
+ )}
- - -
-
密码必须大于6位
+ passwdChangeEvent(e.target.value)} + id="passwd" + placeholder="请输入密码" + className="mb-1 flex-3" + type="password" + /> + +
+ {passwdErrorMsg ? ( +
{passwdErrorMsg}
+ ) : ( + "" + )}
- - - -
-
两次输入的密码不一致
+ sepasswdChangeEvent(e.target.value)} + id="passwd1" + placeholder="再一次输入密码" + className="mb-1 flex-3" + type="password" + /> + +
+ {sepasswdErrorMsg ? ( +
+ 两次输入的密码不一致 +
+ ) : ( + "" + )}
- - - -
-
邮箱格式不正确
+ emailChangeEvent(e.target.value)} + /> + +
+ {emailErrorMsg ? ( +
{emailErrorMsg}
+ ) : ( + "" + )}
- - -
-
无效的邀请码
+ inviteCodeChangeEvent(e.target.value)} + /> + +
+ {inviteCodeErrorMsg ? ( +
+ {inviteCodeErrorMsg} +
+ ) : ( + "" + )}
-
- - -
-
无效的验证码
+
+ refreshCaptcha()} + className="mt-0.5 w-20 h-8 absolute left-63 hover:cursor-pointer" + src={captcha == null ? "#" : captcha} + alt="点击刷新" + /> + verificationCodeEvent(e.target.value)} + id="passwd1" + placeholder="请输入验证码" + className="mb-1 flex-3" + /> + +
+ {/*
无效的验证码
*/}
- 同意用户及隐私协议 + + setUserRegisterValid({ ...userRegisterValid, agree: check }) + } + /> + + 同意用户及隐私协议 +

- +
diff --git a/src/store/useUserStore.ts b/src/store/useUserStore.ts new file mode 100644 index 0000000..757f242 --- /dev/null +++ b/src/store/useUserStore.ts @@ -0,0 +1,12 @@ +import { uuid } from './../lib/keyGen'; +import { create } from 'zustand'; + +interface UserState { + key: string +} + +const useUserStore = create(() => ({ + key: uuid().replaceAll('-','') +})); + +export default useUserStore; \ No newline at end of file diff --git a/src/types/common.ts b/src/types/common.ts new file mode 100644 index 0000000..47d94a0 --- /dev/null +++ b/src/types/common.ts @@ -0,0 +1,5 @@ +export interface Result { + code: number + msg:string + data: T +} \ No newline at end of file diff --git a/src/types/user.ts b/src/types/user.ts new file mode 100644 index 0000000..b7aac9e --- /dev/null +++ b/src/types/user.ts @@ -0,0 +1,8 @@ +export interface UserRegisterT { + username?: string; + password?: string; + email?: string; + inviteCode?: string; + verificationCode?: string; + key?:string +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index dfa644a..177978f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,6 +4,17 @@ import path from "path" import tailwindcss from "@tailwindcss/vite" // https://vite.dev/config/ export default defineConfig({ + server: { + proxy: { + // 1. 匹配所有以 /api 开头的请求 + '/api': { + target: 'http://localhost', // 你的 Java 后端地址 + changeOrigin: true, // 允许跨域 + // 2. 路径重写:如果后端接口没有 /api 前缀,就把它删掉 + rewrite: (path) => path.replace(/^\/api/, '') + }, + } + }, plugins: [ tailwindcss(), react({