diff --git a/package-lock.json b/package-lock.json
index 4b030b8..1d52648 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@tanstack/react-query": "^5.90.21",
+ "@tsparticles/react": "^3.0.0",
"axios": "^1.13.5",
"clsx": "^2.1.1",
"lucide-react": "^0.575.0",
@@ -17,6 +18,7 @@
"react-dropzone": "^15.0.0",
"react-router-dom": "^7.13.0",
"tailwind-merge": "^3.5.0",
+ "tsparticles-slim": "^2.12.0",
"zustand": "^5.0.11"
},
"devDependencies": {
@@ -1455,6 +1457,38 @@
"react": "^18 || ^19"
}
},
+ "node_modules/@tsparticles/engine": {
+ "version": "3.9.1",
+ "resolved": "https://registry.npmjs.org/@tsparticles/engine/-/engine-3.9.1.tgz",
+ "integrity": "sha512-DpdgAhWMZ3Eh2gyxik8FXS6BKZ8vyea+Eu5BC4epsahqTGY9V3JGGJcXC6lRJx6cPMAx1A0FaQAojPF3v6rkmQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/matteobruni"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/tsparticles"
+ },
+ {
+ "type": "buymeacoffee",
+ "url": "https://www.buymeacoffee.com/matteobruni"
+ }
+ ],
+ "hasInstallScript": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@tsparticles/react": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/react/-/react-3.0.0.tgz",
+ "integrity": "sha512-hjGEtTT1cwv6BcjL+GcVgH++KYs52bIuQGW3PWv7z3tMa8g0bd6RI/vWSLj7p//NZ3uTjEIeilYIUPBh7Jfq/Q==",
+ "peerDependencies": {
+ "@tsparticles/engine": "^3.0.2",
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -4369,6 +4403,452 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
+ "node_modules/tsparticles-basic": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-basic/-/tsparticles-basic-2.12.0.tgz",
+ "integrity": "sha512-pN6FBpL0UsIUXjYbiui5+IVsbIItbQGOlwyGV55g6IYJBgdTNXgFX0HRYZGE9ZZ9psEXqzqwLM37zvWnb5AG9g==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/matteobruni"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/tsparticles"
+ },
+ {
+ "type": "buymeacoffee",
+ "url": "https://www.buymeacoffee.com/matteobruni"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0",
+ "tsparticles-move-base": "^2.12.0",
+ "tsparticles-shape-circle": "^2.12.0",
+ "tsparticles-updater-color": "^2.12.0",
+ "tsparticles-updater-opacity": "^2.12.0",
+ "tsparticles-updater-out-modes": "^2.12.0",
+ "tsparticles-updater-size": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-engine": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-engine/-/tsparticles-engine-2.12.0.tgz",
+ "integrity": "sha512-ZjDIYex6jBJ4iMc9+z0uPe7SgBnmb6l+EJm83MPIsOny9lPpetMsnw/8YJ3xdxn8hV+S3myTpTN1CkOVmFv0QQ==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/matteobruni"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/tsparticles"
+ },
+ {
+ "type": "buymeacoffee",
+ "url": "https://www.buymeacoffee.com/matteobruni"
+ }
+ ],
+ "hasInstallScript": true,
+ "license": "MIT"
+ },
+ "node_modules/tsparticles-interaction-external-attract": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-interaction-external-attract/-/tsparticles-interaction-external-attract-2.12.0.tgz",
+ "integrity": "sha512-0roC6D1QkFqMVomcMlTaBrNVjVOpyNzxIUsjMfshk2wUZDAvTNTuWQdUpmsLS4EeSTDN3rzlGNnIuuUQqyBU5w==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-interaction-external-bounce": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-interaction-external-bounce/-/tsparticles-interaction-external-bounce-2.12.0.tgz",
+ "integrity": "sha512-MMcqKLnQMJ30hubORtdq+4QMldQ3+gJu0bBYsQr9BsThsh8/V0xHc1iokZobqHYVP5tV77mbFBD8Z7iSCf0TMQ==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-interaction-external-bubble": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-interaction-external-bubble/-/tsparticles-interaction-external-bubble-2.12.0.tgz",
+ "integrity": "sha512-5kImCSCZlLNccXOHPIi2Yn+rQWTX3sEa/xCHwXW19uHxtILVJlnAweayc8+Zgmb7mo0DscBtWVFXHPxrVPFDUA==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-interaction-external-connect": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-interaction-external-connect/-/tsparticles-interaction-external-connect-2.12.0.tgz",
+ "integrity": "sha512-ymzmFPXz6AaA1LAOL5Ihuy7YSQEW8MzuSJzbd0ES13U8XjiU3HlFqlH6WGT1KvXNw6WYoqrZt0T3fKxBW3/C3A==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-interaction-external-grab": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-interaction-external-grab/-/tsparticles-interaction-external-grab-2.12.0.tgz",
+ "integrity": "sha512-iQF/A947hSfDNqAjr49PRjyQaeRkYgTYpfNmAf+EfME8RsbapeP/BSyF6mTy0UAFC0hK2A2Hwgw72eT78yhXeQ==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-interaction-external-pause": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-interaction-external-pause/-/tsparticles-interaction-external-pause-2.12.0.tgz",
+ "integrity": "sha512-4SUikNpsFROHnRqniL+uX2E388YTtfRWqqqZxRhY0BrijH4z04Aii3YqaGhJxfrwDKkTQlIoM2GbFT552QZWjw==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-interaction-external-push": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-interaction-external-push/-/tsparticles-interaction-external-push-2.12.0.tgz",
+ "integrity": "sha512-kqs3V0dgDKgMoeqbdg+cKH2F+DTrvfCMrPF1MCCUpBCqBiH+TRQpJNNC86EZYHfNUeeLuIM3ttWwIkk2hllR/Q==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-interaction-external-remove": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-interaction-external-remove/-/tsparticles-interaction-external-remove-2.12.0.tgz",
+ "integrity": "sha512-2eNIrv4m1WB2VfSVj46V2L/J9hNEZnMgFc+A+qmy66C8KzDN1G8aJUAf1inW8JVc0lmo5+WKhzex4X0ZSMghBg==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-interaction-external-repulse": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-interaction-external-repulse/-/tsparticles-interaction-external-repulse-2.12.0.tgz",
+ "integrity": "sha512-rSzdnmgljeBCj5FPp4AtGxOG9TmTsK3AjQW0vlyd1aG2O5kSqFjR+FuT7rfdSk9LEJGH5SjPFE6cwbuy51uEWA==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-interaction-external-slow": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-interaction-external-slow/-/tsparticles-interaction-external-slow-2.12.0.tgz",
+ "integrity": "sha512-2IKdMC3om7DttqyroMtO//xNdF0NvJL/Lx7LDo08VpfTgJJozxU+JAUT8XVT7urxhaDzbxSSIROc79epESROtA==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-interaction-particles-attract": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-interaction-particles-attract/-/tsparticles-interaction-particles-attract-2.12.0.tgz",
+ "integrity": "sha512-Hl8qwuwF9aLq3FOkAW+Zomu7Gb8IKs6Y3tFQUQScDmrrSCaeRt2EGklAiwgxwgntmqzL7hbMWNx06CHHcUQKdQ==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-interaction-particles-collisions": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-interaction-particles-collisions/-/tsparticles-interaction-particles-collisions-2.12.0.tgz",
+ "integrity": "sha512-Se9nPWlyPxdsnHgR6ap4YUImAu3W5MeGKJaQMiQpm1vW8lSMOUejI1n1ioIaQth9weKGKnD9rvcNn76sFlzGBA==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-interaction-particles-links": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-interaction-particles-links/-/tsparticles-interaction-particles-links-2.12.0.tgz",
+ "integrity": "sha512-e7I8gRs4rmKfcsHONXMkJnymRWpxHmeaJIo4g2NaDRjIgeb2AcJSWKWZvrsoLnm7zvaf/cMQlbN6vQwCixYq3A==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-move-base": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-move-base/-/tsparticles-move-base-2.12.0.tgz",
+ "integrity": "sha512-oSogCDougIImq+iRtIFJD0YFArlorSi8IW3HD2gO3USkH+aNn3ZqZNTqp321uB08K34HpS263DTbhLHa/D6BWw==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-move-parallax": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-move-parallax/-/tsparticles-move-parallax-2.12.0.tgz",
+ "integrity": "sha512-58CYXaX8Ih5rNtYhpnH0YwU4Ks7gVZMREGUJtmjhuYN+OFr9FVdF3oDIJ9N6gY5a5AnAKz8f5j5qpucoPRcYrQ==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-particles.js": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-particles.js/-/tsparticles-particles.js-2.12.0.tgz",
+ "integrity": "sha512-LyOuvYdhbUScmA4iDgV3LxA0HzY1DnOwQUy3NrPYO393S2YwdDjdwMod6Btq7EBUjg9FVIh+sZRizgV5elV2dg==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/matteobruni"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/tsparticles"
+ },
+ {
+ "type": "buymeacoffee",
+ "url": "https://www.buymeacoffee.com/matteobruni"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-plugin-easing-quad": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-plugin-easing-quad/-/tsparticles-plugin-easing-quad-2.12.0.tgz",
+ "integrity": "sha512-2mNqez5pydDewMIUWaUhY5cNQ80IUOYiujwG6qx9spTq1D6EEPLbRNAEL8/ecPdn2j1Um3iWSx6lo340rPkv4Q==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/matteobruni"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/tsparticles"
+ },
+ {
+ "type": "buymeacoffee",
+ "url": "https://www.buymeacoffee.com/matteobruni"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-shape-circle": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-shape-circle/-/tsparticles-shape-circle-2.12.0.tgz",
+ "integrity": "sha512-L6OngbAlbadG7b783x16ns3+SZ7i0SSB66M8xGa5/k+YcY7zm8zG0uPt1Hd+xQDR2aNA3RngVM10O23/Lwk65Q==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-shape-image": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-shape-image/-/tsparticles-shape-image-2.12.0.tgz",
+ "integrity": "sha512-iCkSdUVa40DxhkkYjYuYHr9MJGVw+QnQuN5UC+e/yBgJQY+1tQL8UH0+YU/h0GHTzh5Sm+y+g51gOFxHt1dj7Q==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-shape-line": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-shape-line/-/tsparticles-shape-line-2.12.0.tgz",
+ "integrity": "sha512-RcpKmmpKlk+R8mM5wA2v64Lv1jvXtU4SrBDv3vbdRodKbKaWGGzymzav1Q0hYyDyUZgplEK/a5ZwrfrOwmgYGA==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-shape-polygon": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-shape-polygon/-/tsparticles-shape-polygon-2.12.0.tgz",
+ "integrity": "sha512-5YEy7HVMt1Obxd/jnlsjajchAlYMr9eRZWN+lSjcFSH6Ibra7h59YuJVnwxOxAobpijGxsNiBX0PuGQnB47pmA==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-shape-square": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-shape-square/-/tsparticles-shape-square-2.12.0.tgz",
+ "integrity": "sha512-33vfajHqmlODKaUzyPI/aVhnAOT09V7nfEPNl8DD0cfiNikEuPkbFqgJezJuE55ebtVo7BZPDA9o7GYbWxQNuw==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-shape-star": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-shape-star/-/tsparticles-shape-star-2.12.0.tgz",
+ "integrity": "sha512-4sfG/BBqm2qBnPLASl2L5aBfCx86cmZLXeh49Un+TIR1F5Qh4XUFsahgVOG0vkZQa+rOsZPEH04xY5feWmj90g==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-shape-text": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-shape-text/-/tsparticles-shape-text-2.12.0.tgz",
+ "integrity": "sha512-v2/FCA+hyTbDqp2ymFOe97h/NFb2eezECMrdirHWew3E3qlvj9S/xBibjbpZva2gnXcasBwxn0+LxKbgGdP0rA==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-slim": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-slim/-/tsparticles-slim-2.12.0.tgz",
+ "integrity": "sha512-27w9aGAAAPKHvP4LHzWFpyqu7wKyulayyaZ/L6Tuuejy4KP4BBEB4rY5GG91yvAPsLtr6rwWAn3yS+uxnBDpkA==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/matteobruni"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/tsparticles"
+ },
+ {
+ "type": "buymeacoffee",
+ "url": "https://www.buymeacoffee.com/matteobruni"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-basic": "^2.12.0",
+ "tsparticles-engine": "^2.12.0",
+ "tsparticles-interaction-external-attract": "^2.12.0",
+ "tsparticles-interaction-external-bounce": "^2.12.0",
+ "tsparticles-interaction-external-bubble": "^2.12.0",
+ "tsparticles-interaction-external-connect": "^2.12.0",
+ "tsparticles-interaction-external-grab": "^2.12.0",
+ "tsparticles-interaction-external-pause": "^2.12.0",
+ "tsparticles-interaction-external-push": "^2.12.0",
+ "tsparticles-interaction-external-remove": "^2.12.0",
+ "tsparticles-interaction-external-repulse": "^2.12.0",
+ "tsparticles-interaction-external-slow": "^2.12.0",
+ "tsparticles-interaction-particles-attract": "^2.12.0",
+ "tsparticles-interaction-particles-collisions": "^2.12.0",
+ "tsparticles-interaction-particles-links": "^2.12.0",
+ "tsparticles-move-base": "^2.12.0",
+ "tsparticles-move-parallax": "^2.12.0",
+ "tsparticles-particles.js": "^2.12.0",
+ "tsparticles-plugin-easing-quad": "^2.12.0",
+ "tsparticles-shape-circle": "^2.12.0",
+ "tsparticles-shape-image": "^2.12.0",
+ "tsparticles-shape-line": "^2.12.0",
+ "tsparticles-shape-polygon": "^2.12.0",
+ "tsparticles-shape-square": "^2.12.0",
+ "tsparticles-shape-star": "^2.12.0",
+ "tsparticles-shape-text": "^2.12.0",
+ "tsparticles-updater-color": "^2.12.0",
+ "tsparticles-updater-life": "^2.12.0",
+ "tsparticles-updater-opacity": "^2.12.0",
+ "tsparticles-updater-out-modes": "^2.12.0",
+ "tsparticles-updater-rotate": "^2.12.0",
+ "tsparticles-updater-size": "^2.12.0",
+ "tsparticles-updater-stroke-color": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-updater-color": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-updater-color/-/tsparticles-updater-color-2.12.0.tgz",
+ "integrity": "sha512-KcG3a8zd0f8CTiOrylXGChBrjhKcchvDJjx9sp5qpwQK61JlNojNCU35xoaSk2eEHeOvFjh0o3CXWUmYPUcBTQ==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-updater-life": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-updater-life/-/tsparticles-updater-life-2.12.0.tgz",
+ "integrity": "sha512-J7RWGHAZkowBHpcLpmjKsxwnZZJ94oGEL2w+wvW1/+ZLmAiFFF6UgU0rHMC5CbHJT4IPx9cbkYMEHsBkcRJ0Bw==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-updater-opacity": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-updater-opacity/-/tsparticles-updater-opacity-2.12.0.tgz",
+ "integrity": "sha512-YUjMsgHdaYi4HN89LLogboYcCi1o9VGo21upoqxq19yRy0hRCtx2NhH22iHF/i5WrX6jqshN0iuiiNefC53CsA==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-updater-out-modes": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-updater-out-modes/-/tsparticles-updater-out-modes-2.12.0.tgz",
+ "integrity": "sha512-owBp4Gk0JNlSrmp12XVEeBroDhLZU+Uq3szbWlHGSfcR88W4c/0bt0FiH5bHUqORIkw+m8O56hCjbqwj69kpOQ==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-updater-rotate": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-updater-rotate/-/tsparticles-updater-rotate-2.12.0.tgz",
+ "integrity": "sha512-waOFlGFmEZOzsQg4C4VSejNVXGf4dMf3fsnQrEROASGf1FCd8B6WcZau7JtXSTFw0OUGuk8UGz36ETWN72DkCw==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-updater-size": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-updater-size/-/tsparticles-updater-size-2.12.0.tgz",
+ "integrity": "sha512-B0yRdEDd/qZXCGDL/ussHfx5YJ9UhTqNvmS5X2rR2hiZhBAE2fmsXLeWkdtF2QusjPeEqFDxrkGiLOsh6poqRA==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
+ "node_modules/tsparticles-updater-stroke-color": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/tsparticles-updater-stroke-color/-/tsparticles-updater-stroke-color-2.12.0.tgz",
+ "integrity": "sha512-MPou1ZDxsuVq6SN1fbX+aI5yrs6FyP2iPCqqttpNbWyL+R6fik1rL0ab/x02B57liDXqGKYomIbBQVP3zUTW1A==",
+ "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
+ "license": "MIT",
+ "dependencies": {
+ "tsparticles-engine": "^2.12.0"
+ }
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
diff --git a/package.json b/package.json
index d61549e..e68f16b 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
},
"dependencies": {
"@tanstack/react-query": "^5.90.21",
+ "@tsparticles/react": "^3.0.0",
"axios": "^1.13.5",
"clsx": "^2.1.1",
"lucide-react": "^0.575.0",
@@ -19,6 +20,7 @@
"react-dropzone": "^15.0.0",
"react-router-dom": "^7.13.0",
"tailwind-merge": "^3.5.0",
+ "tsparticles-slim": "^2.12.0",
"zustand": "^5.0.11"
},
"devDependencies": {
@@ -32,9 +34,9 @@
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
- "typescript": "~5.9.3",
"postcss": "^8.5.5",
"tailwindcss": "^3.4.17",
+ "typescript": "~5.9.3",
"typescript-eslint": "^8.48.0",
"vite": "^7.3.1"
}
diff --git a/src/App.css b/src/App.css
index 38a140b..1e408eb 100644
--- a/src/App.css
+++ b/src/App.css
@@ -2,7 +2,7 @@
/* max-width: 1280px; */
margin: 0 auto;
padding-inline: 1rem;
- text-align: center;
+ text-align: start;
}
.logo {
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
index 18714d5..843c1e4 100644
--- a/src/components/Layout.tsx
+++ b/src/components/Layout.tsx
@@ -79,7 +79,7 @@ export const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children })
to={href}
onClick={() => setSidebarOpen(false)}
className={cn(
- 'group flex items-center px-3 py-2.5 rounded-xl text-sm font-medium',
+ 'group flex items-center px-3 py-2.5 gap-4 rounded-xl text-sm font-medium',
'transition-all duration-150',
active
? 'bg-primary-50 text-primary-700'
diff --git a/src/pages/AnalyticsPage.tsx b/src/pages/AnalyticsPage.tsx
index 62ac1d2..04f5e7d 100644
--- a/src/pages/AnalyticsPage.tsx
+++ b/src/pages/AnalyticsPage.tsx
@@ -441,7 +441,7 @@ export const AnalyticsPage: React.FC = () => {
if (isLoading) {
return (
-
+
{/* Header skeleton */}
@@ -476,7 +476,7 @@ export const AnalyticsPage: React.FC = () => {
if (!data) {
return (
-
+
diff --git a/src/pages/AppointmentsPage.tsx b/src/pages/AppointmentsPage.tsx
index ed36c5e..b3de485 100644
--- a/src/pages/AppointmentsPage.tsx
+++ b/src/pages/AppointmentsPage.tsx
@@ -191,7 +191,7 @@ export const AppointmentsPage: React.FC = () => {
const bookingEnabledChatbots = chatbots.filter(c => c.booking_enabled)
return (
-
+
{/* Header */}
diff --git a/src/pages/CampaignsPage.tsx b/src/pages/CampaignsPage.tsx
index b17779b..62a8a6f 100644
--- a/src/pages/CampaignsPage.tsx
+++ b/src/pages/CampaignsPage.tsx
@@ -173,7 +173,7 @@ export const CampaignsPage: React.FC = () => {
const sentTotal = campaigns.filter(c => c.status === 'sent').reduce((sum, c) => sum + c.sent_count, 0)
return (
-
+
{/* Confirm send modal */}
{confirmSendId && (() => {
diff --git a/src/pages/LeadsPage.tsx b/src/pages/LeadsPage.tsx
index cdd9cf9..a12ac98 100644
--- a/src/pages/LeadsPage.tsx
+++ b/src/pages/LeadsPage.tsx
@@ -137,8 +137,7 @@ export const LeadsPage: React.FC = () => {
}
return (
-
-
+
{notesLead && (
{
- const navigate = useNavigate()
- const [search, setSearch] = useState('')
- const [debouncedSearch, setDebouncedSearch] = useState('')
- const [category, setCategory] = useState('')
- const [industry, setIndustry] = useState('')
- const [page, setPage] = useState(1)
- const [showFilters, setShowFilters] = useState(false)
+ const navigate = useNavigate();
+ const [search, setSearch] = useState("");
+ const [debouncedSearch, setDebouncedSearch] = useState("");
+ const [category, setCategory] = useState("");
+ const [industry, setIndustry] = useState("");
+ const [page, setPage] = useState(1);
+ const [showFilters, setShowFilters] = useState(false);
- // Debounce search
- const searchTimeout = React.useRef>()
- const handleSearch = (value: string) => {
- setSearch(value)
- clearTimeout(searchTimeout.current)
- searchTimeout.current = setTimeout(() => {
- setDebouncedSearch(value)
- setPage(1)
- }, 300)
- }
+ // Debounce search
+ const searchTimeout = React.useRef>();
+ const handleSearch = (value: string) => {
+ setSearch(value);
+ clearTimeout(searchTimeout.current);
+ searchTimeout.current = setTimeout(() => {
+ setDebouncedSearch(value);
+ setPage(1);
+ }, 300);
+ };
- const { data, isLoading } = useQuery({
- queryKey: ['marketplace', debouncedSearch, category, industry, page],
- queryFn: () => marketplaceAPI.list({ search: debouncedSearch, category, industry, page, limit: 20 }),
- })
+ const { data, isLoading } = useQuery({
+ queryKey: ["marketplace", debouncedSearch, category, industry, page],
+ queryFn: () =>
+ marketplaceAPI.list({
+ search: debouncedSearch,
+ category,
+ industry,
+ page,
+ limit: 20,
+ }),
+ });
- const totalPages = data ? Math.ceil(data.total / 20) : 0
- const hasActiveFilters = category !== '' || industry !== ''
+ const totalPages = data ? Math.ceil(data.total / 20) : 0;
+ const hasActiveFilters = category !== "" || industry !== "";
- return (
-
- {/* Page Header */}
-
-
-
-
-
-
-
-
- AI Chatbot Marketplace
-
-
-
- Discover and interact with AI-powered chatbots built by businesses — ready to answer your questions instantly.
-
-
-
-
-
-
- {/* Search & Filter Bar */}
-
-
-
-
-
handleSearch(e.target.value)}
- placeholder="Search chatbots by name or description..."
- className="w-full pl-10 pr-4 py-2.5 border border-gray-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-400 bg-white shadow-sm transition-all placeholder-gray-400"
- />
- {search && (
-
- )}
-
-
-
-
- {/* Expandable filter section */}
- {showFilters && (
-
- {/* Category filter — pill buttons since list is manageable */}
-
-
Category
-
-
- {CATEGORIES.map(c => (
-
- ))}
-
-
-
- {/* Industry filter — select dropdown since list is long */}
-
-
Industry
-
-
-
- {hasActiveFilters && (
-
- )}
-
- )}
-
-
- {/* Results */}
- {isLoading ? (
-
- {Array.from({ length: 6 }).map((_, i) => (
-
- ))}
-
- ) : !data?.chatbots?.length ? (
-
}
- title="No chatbots found"
- description={
- hasActiveFilters || debouncedSearch
- ? "Try adjusting your filters or search query."
- : "Be the first to publish your AI chatbot to the marketplace!"
- }
- action={
- hasActiveFilters || debouncedSearch ? (
-
- ) : (
-
- )
- }
- />
- ) : (
- <>
-
-
- {data.total} chatbot{data.total !== 1 ? 's' : ''} available
-
- {hasActiveFilters && (
-
- )}
-
-
-
- {data.chatbots.map((chatbot, i) => (
- navigate(`/marketplace/${chatbot.id}`)}
- />
- ))}
-
-
- {/* Pagination */}
- {data.total > 20 && (
-
-
-
- {Array.from({ length: totalPages }, (_, i) => i + 1)
- .filter(p => p === 1 || p === totalPages || Math.abs(p - page) <= 1)
- .reduce<(number | 'ellipsis')[]>((acc, p, idx, arr) => {
- if (idx > 0 && p - (arr[idx - 1] as number) > 1) acc.push('ellipsis')
- acc.push(p)
- return acc
- }, [])
- .map((p, idx) =>
- p === 'ellipsis' ? (
-
- …
-
- ) : (
-
- )
- )}
-
-
-
- )}
- >
- )}
+ return (
+
+ {/* Page Header */}
+
+
+
+
+
+
+
+
+ AI Chatbot Marketplace
+
+
+ Discover and interact with AI-powered chatbots built by businesses
+ — ready to answer your questions instantly.
+
+
- )
-}
+
+ {/* Content */}
+
+ {/* Search & Filter Bar */}
+
+
+
+
+
handleSearch(e.target.value)}
+ placeholder="Search chatbots by name or description..."
+ className="w-full pl-10 pr-4 py-2.5 border border-gray-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-400 bg-white shadow-sm transition-all placeholder-gray-400"
+ />
+ {search && (
+
+ )}
+
+
+
+
+ {/* Expandable filter section */}
+ {showFilters && (
+
+ {/* Category filter — pill buttons since list is manageable */}
+
+
+ Category
+
+
+
+ {CATEGORIES.map((c) => (
+
+ ))}
+
+
+
+ {/* Industry filter — select dropdown since list is long */}
+
+
+ Industry
+
+
+
+
+ {hasActiveFilters && (
+
+ )}
+
+ )}
+
+
+ {/* Results */}
+ {isLoading ? (
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+ ))}
+
+ ) : !data?.chatbots?.length ? (
+
}
+ title="No chatbots found"
+ description={
+ hasActiveFilters || debouncedSearch
+ ? "Try adjusting your filters or search query."
+ : "Be the first to publish your AI chatbot to the marketplace!"
+ }
+ action={
+ hasActiveFilters || debouncedSearch ? (
+
+ ) : (
+
+ )
+ }
+ />
+ ) : (
+ <>
+
+
+ {data.total} chatbot{data.total !== 1 ? "s" : ""} available
+
+ {hasActiveFilters && (
+
+ )}
+
+
+
+ {data.chatbots.map((chatbot, i) => (
+ navigate(`/marketplace/${chatbot.id}`)}
+ />
+ ))}
+
+
+ {/* Pagination */}
+ {data.total > 20 && (
+
+
+
+ {Array.from({ length: totalPages }, (_, i) => i + 1)
+ .filter(
+ (p) =>
+ p === 1 || p === totalPages || Math.abs(p - page) <= 1,
+ )
+ .reduce<(number | "ellipsis")[]>((acc, p, idx, arr) => {
+ if (idx > 0 && p - (arr[idx - 1] as number) > 1)
+ acc.push("ellipsis");
+ acc.push(p);
+ return acc;
+ }, [])
+ .map((p, idx) =>
+ p === "ellipsis" ? (
+
+ …
+
+ ) : (
+
+ ),
+ )}
+
+
+
+ )}
+ >
+ )}
+
+
+ );
+};
// ═══════════════════════════════════════════════════════════════════════════════
// MARKETPLACE CARD — shows logo when available
// ═══════════════════════════════════════════════════════════════════════════════
-const ChatbotMarketplaceCard: React.FC<{ chatbot: ChatbotPublic; onClick: () => void; index: number }> = ({ chatbot, onClick, index }) => (
+const ChatbotMarketplaceCard: React.FC<{
+ chatbot: ChatbotPublic;
+ onClick: () => void;
+ index: number;
+}> = ({ chatbot, onClick, index }) => (
+
+ {/* Colored accent top bar — thicker and with gradient */}
- {/* Colored accent top bar — thicker and with gradient */}
-
+ className="h-1.5 w-full"
+ style={{
+ background: `linear-gradient(90deg, ${chatbot.primary_color}, ${chatbot.primary_color}aa)`,
+ }}
+ />
-
- {/* Header row */}
-
- {chatbot.logo_url ? (
-

- ) : (
-
-
-
- )}
-
-
- {chatbot.name}
-
- {chatbot.company_name && (
-
by {chatbot.company_name}
- )}
-
-
-
- {/* Description */}
- {chatbot.description ? (
-
{chatbot.description}
- ) : (
-
- )}
-
- {/* Stats row */}
-
- {chatbot.average_rating && chatbot.average_rating > 0 && (
-
-
- {chatbot.average_rating.toFixed(1)}
-
- )}
-
-
- {chatbot.total_conversations.toLocaleString()}
-
- {chatbot.category && (
-
- {chatbot.category}
-
- )}
-
+
+ {/* Header row */}
+
+ {chatbot.logo_url ? (
+

+ ) : (
+
+
+
+ )}
+
+
+ {chatbot.name}
+
+ {chatbot.company_name && (
+
+ by {chatbot.company_name}
+
+ )}
+
- {/* Hover overlay: "Chat now" CTA */}
-
+ {/* Description */}
+ {chatbot.description ? (
+
+ {chatbot.description}
+
+ ) : (
+
+ )}
+
+ {/* Stats row */}
+
+ {chatbot.average_rating && chatbot.average_rating > 0 && (
+
+
+ {chatbot.average_rating.toFixed(1)}
+
+ )}
+
+
+ {chatbot.total_conversations.toLocaleString()}
+
+ {chatbot.category && (
+
+ {chatbot.category}
+
+ )}
+
-)
+ {/* Hover overlay: "Chat now" CTA */}
+
+
+);
// ═══════════════════════════════════════════════════════════════════════════════
// CHATBOT DETAIL PAGE — shows logo in header + passes to ChatInterface
// ═══════════════════════════════════════════════════════════════════════════════
export const ChatbotDetailPage: React.FC = () => {
- const { id } = useParams<{ id: string }>()
- const navigate = useNavigate()
+ const { id } = useParams<{ id: string }>();
+ const navigate = useNavigate();
- const { data: chatbot, isLoading, error } = useQuery({
- queryKey: ['marketplace-chatbot', id],
- queryFn: () => marketplaceAPI.get(id!),
- enabled: !!id,
- })
-
- if (isLoading) {
- return (
-
-
-
- )
- }
-
- if (error || !chatbot) {
- return (
-
-
}
- title="Chatbot not found"
- description="This chatbot may have been unpublished or removed."
- action={
-
- }
- />
-
- )
- }
+ const {
+ data: chatbot,
+ isLoading,
+ error,
+ } = useQuery({
+ queryKey: ["marketplace-chatbot", id],
+ queryFn: () => marketplaceAPI.get(id!),
+ enabled: !!id,
+ });
+ if (isLoading) {
return (
-
- {/* Back link */}
-
+
+
+
+ );
+ }
- {/* Chatbot info card */}
-
- {/* Accent bar */}
-
-
-
- {chatbot.logo_url ? (
-

- ) : (
-
-
-
- )}
-
-
{chatbot.name}
- {chatbot.company_name && (
-
by {chatbot.company_name}
- )}
-
- {chatbot.average_rating && chatbot.average_rating > 0 && (
-
-
- {chatbot.average_rating.toFixed(1)}
-
- )}
-
-
- {chatbot.total_conversations.toLocaleString()} conversations
-
- {chatbot.category && (
-
- {chatbot.category}
-
- )}
-
-
-
+ if (error || !chatbot) {
+ return (
+
+
}
+ title="Chatbot not found"
+ description="This chatbot may have been unpublished or removed."
+ action={
+
+ }
+ />
+
+ );
+ }
- {chatbot.description && (
-
{chatbot.description}
- )}
-
+ return (
+
+ {/* Back link */}
+
+
+ {/* Chatbot info card */}
+
+ {/* Accent bar */}
+
+
+
+ {chatbot.logo_url ? (
+

+ ) : (
+
+
+
+ )}
+
+
+ {chatbot.name}
+
+ {chatbot.company_name && (
+
+ by {chatbot.company_name}
+
+ )}
+
+ {chatbot.average_rating && chatbot.average_rating > 0 && (
+
+
+ {chatbot.average_rating.toFixed(1)}
+
+ )}
+
+
+ {chatbot.total_conversations.toLocaleString()} conversations
+
+ {chatbot.category && (
+
+ {chatbot.category}
+
+ )}
+
+
- {/* Chat */}
-
-
-
+ {chatbot.description && (
+
+ {chatbot.description}
+
+ )}
- )
-}
+
+
+ {/* Chat */}
+
+
+
+
+ );
+};
diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx
index 422d0c9..956a63f 100644
--- a/src/pages/SettingsPage.tsx
+++ b/src/pages/SettingsPage.tsx
@@ -1,305 +1,508 @@
-import React, { useState, useCallback } from 'react'
-import { useQuery } from '@tanstack/react-query'
-import { useNavigate, useLocation, Link } from 'react-router-dom'
-import { billingAPI, authAPI } from '@/services/api'
-import { useAuthStore } from '@/store/authStore'
-import { Button, Card, Input } from '@/components/ui'
-import { useToast } from '@/contexts/ToastContext'
-import { useThemeStore } from '@/store/themeStore'
-import { getPlanColor, formatDate } from '@/lib/utils'
-import { CreditCard, User, ExternalLink, AlertTriangle, Moon, Sun } from 'lucide-react'
+import React, { useState, useCallback } from "react";
+import { useQuery } from "@tanstack/react-query";
+import { useNavigate, useLocation, Link } from "react-router-dom";
+import { billingAPI, authAPI } from "@/services/api";
+import { useAuthStore } from "@/store/authStore";
+import { Button, Card, Input } from "@/components/ui";
+import { useToast } from "@/contexts/ToastContext";
+import { useThemeStore } from "@/store/themeStore";
+import { getPlanColor, formatDate } from "@/lib/utils";
+import {
+ CreditCard,
+ User,
+ ExternalLink,
+ AlertTriangle,
+ Moon,
+ Sun,
+} from "lucide-react";
export const SettingsPage: React.FC = () => {
- const navigate = useNavigate()
- const location = useLocation()
- const { success: showToast, error: showError } = useToast()
- const { isDark, toggle: toggleTheme } = useThemeStore()
+ const navigate = useNavigate();
+ const location = useLocation();
+ const { success: showToast, error: showError } = useToast();
+ const { isDark, toggle: toggleTheme } = useThemeStore();
- const tab: 'profile' | 'billing' = location.pathname === '/settings/billing' ? 'billing' : 'profile'
+ const tab: "profile" | "billing" =
+ location.pathname === "/settings/billing" ? "billing" : "profile";
- const handleTabChange = useCallback((newTab: 'profile' | 'billing') => {
- const newPath = newTab === 'billing' ? '/settings/billing' : '/settings'
- if (location.pathname !== newPath) {
- navigate(newPath, { replace: true })
- }
- }, [navigate, location.pathname])
+ const handleTabChange = useCallback(
+ (newTab: "profile" | "billing") => {
+ const newPath = newTab === "billing" ? "/settings/billing" : "/settings";
+ if (location.pathname !== newPath) {
+ navigate(newPath, { replace: true });
+ }
+ },
+ [navigate, location.pathname],
+ );
return (
-
-
-
Settings
-
-
-
- {/* Tabs */}
-
- {[
- { id: 'profile' as const, label: 'Profile', icon: User },
- { id: 'billing' as const, label: 'Billing', icon: CreditCard },
- ].map(({ id, label, icon: Icon }) => (
-
- ))}
-
-
- {tab === 'profile' &&
}
- {tab === 'billing' &&
}
+
+
+
Settings
+
- )
-}
-const ProfileSettings: React.FC<{ onToast: (msg: string) => void; onError: (msg: string) => void }> = ({ onToast, onError }) => {
- const { user, setAuth, token, logout } = useAuthStore()
- const navigate = useNavigate()
- const [companyName, setCompanyName] = useState(user?.company_name || '')
- const [currentPassword, setCurrentPassword] = useState('')
- const [newPassword, setNewPassword] = useState('')
- const [saving, setSaving] = useState(false)
- const [showDeleteModal, setShowDeleteModal] = useState(false)
- const [deleteConfirm, setDeleteConfirm] = useState('')
- const [deleting, setDeleting] = useState(false)
+ {/* Tabs */}
+
+ {[
+ { id: "profile" as const, label: "Profile", icon: User },
+ { id: "billing" as const, label: "Billing", icon: CreditCard },
+ ].map(({ id, label, icon: Icon }) => (
+
+ ))}
+
+
+ {tab === "profile" && (
+
+ )}
+ {tab === "billing" && (
+
+ )}
+
+ );
+};
+
+const ProfileSettings: React.FC<{
+ onToast: (msg: string) => void;
+ onError: (msg: string) => void;
+}> = ({ onToast, onError }) => {
+ const { user, setAuth, token, logout } = useAuthStore();
+ const navigate = useNavigate();
+ const [companyName, setCompanyName] = useState(user?.company_name || "");
+ const [currentPassword, setCurrentPassword] = useState("");
+ const [newPassword, setNewPassword] = useState("");
+ const [saving, setSaving] = useState(false);
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
+ const [deleteConfirm, setDeleteConfirm] = useState("");
+ const [deleting, setDeleting] = useState(false);
const handleSave = async () => {
- setSaving(true)
+ setSaving(true);
try {
- const payload: { company_name?: string; current_password?: string; new_password?: string } = {}
- if (companyName !== user?.company_name) payload.company_name = companyName
+ const payload: {
+ company_name?: string;
+ current_password?: string;
+ new_password?: string;
+ } = {};
+ if (companyName !== user?.company_name)
+ payload.company_name = companyName;
if (newPassword) {
- payload.current_password = currentPassword
- payload.new_password = newPassword
+ payload.current_password = currentPassword;
+ payload.new_password = newPassword;
}
if (Object.keys(payload).length === 0) {
- onToast('No changes to save')
- return
+ onToast("No changes to save");
+ return;
}
- const updated = await authAPI.updateProfile(payload)
- setAuth(updated, token || '')
- setCurrentPassword('')
- setNewPassword('')
- onToast('Profile updated successfully')
+ const updated = await authAPI.updateProfile(payload);
+ setAuth(updated, token || "");
+ setCurrentPassword("");
+ setNewPassword("");
+ onToast("Profile updated successfully");
} catch (err) {
- const e = err as { response?: { data?: { detail?: string } } }
- onError(e.response?.data?.detail || 'Failed to update profile')
+ const e = err as { response?: { data?: { detail?: string } } };
+ onError(e.response?.data?.detail || "Failed to update profile");
} finally {
- setSaving(false)
+ setSaving(false);
}
- }
+ };
const handleDeleteAccount = async () => {
- if (deleteConfirm !== 'DELETE') return
- setDeleting(true)
+ if (deleteConfirm !== "DELETE") return;
+ setDeleting(true);
try {
- await authAPI.deleteAccount()
- logout()
- navigate('/')
+ await authAPI.deleteAccount();
+ logout();
+ navigate("/");
} catch (err) {
- const e = err as { response?: { data?: { detail?: string } } }
- onError(e.response?.data?.detail || 'Failed to delete account')
- setDeleting(false)
+ const e = err as { response?: { data?: { detail?: string } } };
+ onError(e.response?.data?.detail || "Failed to delete account");
+ setDeleting(false);
}
- }
+ };
return (
-
-
- Profile Information
-
- setCompanyName(e.target.value)}
- placeholder="Your company name"
- />
-
-
-
-
- {user?.plan || 'free'}
-
-
- Manage plan
-
-
+
+
+ Profile Information
+
+ setCompanyName(e.target.value)}
+ placeholder="Your company name"
+ />
+
+
+
+
+ {user?.plan || "free"}
+
+
+ Manage plan
+
-
+
+
-
- Change Password
- setCurrentPassword(e.target.value)}
- placeholder="Enter current password"
- />
- setNewPassword(e.target.value)}
- placeholder="Min 8 characters"
- hint="Leave blank to keep current password"
- />
-
+
+ Change Password
+ setCurrentPassword(e.target.value)}
+ placeholder="Enter current password"
+ />
+ setNewPassword(e.target.value)}
+ placeholder="Min 8 characters"
+ hint="Leave blank to keep current password"
+ />
+
-