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.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.name} +

+ {chatbot.company_name && ( +

+ by {chatbot.company_name} +

+ )}
+
- {/* Hover overlay: "Chat now" CTA */} -
-
- Chat now → -
-
+ {/* 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 */} +
+
+ Chat now → +
+
+
+); // ═══════════════════════════════════════════════════════════════════════════════ // 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.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.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" + /> +
- +
- {/* Danger Zone */} - -

- - Danger Zone -

-

- Permanently delete your account, all chatbots, documents, and data. This cannot be undone. -

- -
+ {/* Danger Zone */} + +

+ + Danger Zone +

+

+ Permanently delete your account, all chatbots, documents, and data. + This cannot be undone. +

+ +
- {/* Delete Account Modal */} - {showDeleteModal && ( -
-
-

Delete Account

-

- This will permanently delete your account and all associated data including chatbots, documents, conversations, and leads. - This action cannot be undone. -

-

Type DELETE to confirm:

- setDeleteConfirm(e.target.value)} - placeholder="DELETE" - /> -
- - -
+ {/* Delete Account Modal */} + {showDeleteModal && ( +
+
+

+ Delete Account +

+

+ This will permanently delete your account and all associated data + including chatbots, documents, conversations, and leads. + + {" "} + This action cannot be undone. + +

+

+ Type DELETE to confirm: +

+ setDeleteConfirm(e.target.value)} + placeholder="DELETE" + /> +
+ +
- )} -
- ) -} +
+ )} +
+ ); +}; -const BillingSettings: React.FC<{ onToast: (msg: string) => void; onError: (msg: string) => void }> = ({ onError }) => { - const navigate = useNavigate() - const [loading, setLoading] = useState(false) +const BillingSettings: React.FC<{ + onToast: (msg: string) => void; + onError: (msg: string) => void; +}> = ({ onError }) => { + const navigate = useNavigate(); + const [loading, setLoading] = useState(false); const { data: subscription } = useQuery({ - queryKey: ['subscription'], + queryKey: ["subscription"], queryFn: billingAPI.getSubscription, - }) + }); const handlePortal = async () => { - setLoading(true) + setLoading(true); try { - const { url } = await billingAPI.createPortal(window.location.href) - window.location.href = url + const { url } = await billingAPI.createPortal(window.location.href); + window.location.href = url; } catch (err) { - const e = err as { response?: { data?: { detail?: string } } } - onError(e.response?.data?.detail || 'Failed to open billing portal') + const e = err as { response?: { data?: { detail?: string } } }; + onError(e.response?.data?.detail || "Failed to open billing portal"); } finally { - setLoading(false) + setLoading(false); } - } + }; - const plan = subscription?.plan || 'free' - const isPaid = plan !== 'free' + const plan = subscription?.plan || "free"; + const isPaid = plan !== "free"; - const planFeatures: Record = { - free: { published: '1', conversations: '100/month', codeExport: '❌ Agency+ only' }, - starter: { published: '1', conversations: '1,500/month', codeExport: '❌ Agency+ only' }, - business: { published: '3', conversations: '5,000/month', codeExport: '❌ Agency+ only' }, - agency: { published: 'Unlimited', conversations: '20,000/month', codeExport: '✅ Included' }, - enterprise: { published: 'Unlimited', conversations: 'Unlimited', codeExport: '✅ Included' }, - } - const features = planFeatures[plan] || planFeatures.free + const planFeatures: Record< + string, + { published: string; conversations: string; codeExport: string } + > = { + free: { + published: "1", + conversations: "100/month", + codeExport: "❌ Agency+ only", + }, + starter: { + published: "1", + conversations: "1,500/month", + codeExport: "❌ Agency+ only", + }, + business: { + published: "3", + conversations: "5,000/month", + codeExport: "❌ Agency+ only", + }, + agency: { + published: "Unlimited", + conversations: "20,000/month", + codeExport: "✅ Included", + }, + enterprise: { + published: "Unlimited", + conversations: "Unlimited", + codeExport: "✅ Included", + }, + }; + const features = planFeatures[plan] || planFeatures.free; return ( -
- -

Current Plan

-
-
- - {plan} - -

- Status:{' '} - - {subscription?.status || 'active'} +

+ {/* Current Plan Card - Version with hover effect */} + +
+
+
+
+
+
+ + + +
+

+ Current Plan +

+
+ +
+ + + {plan} -

+
+ Status: + + + {subscription?.status === "active" + ? "Active" + : subscription?.status || "Active"} + +
+
+ {isPaid && subscription?.current_period_end && ( -
-

Renews on

-

{formatDate(subscription.current_period_end)}

-
+
+

+ Renewal Date +

+

+ {formatDate(subscription.current_period_end)} +

+
)}
-
+
{!isPaid ? ( - + ) : ( - + )}
- +
+ - {/* Plan features */} - -

Plan Features

-
+ {/* Plan Features Card - Enhanced Version */} + +
+
+
+ + + +
+

Plan Features

+
+ +
{[ - { label: 'Chatbots published', value: features.published }, - { label: 'Conversations/month', value: features.conversations }, - { label: 'Code export', value: features.codeExport }, - ].map(({ label, value }) => ( -
- {label} - {value} -
+ { + label: "Chatbots published", + value: features.published, + suffix: "chatbot(s)", + }, + { + label: "Conversations / month", + value: features.conversations, + suffix: "conversations", + }, + { + label: "Code export", + value: features.codeExport, + highlight: features.codeExport, + }, + ].map(({ label, value, suffix, highlight }) => ( +
+ + {label} + + + {value}{" "} + {suffix && ( + + {suffix} + + )} + +
))}
- -
- ) -} + +
+

+ {isPaid + ? "💳 Simplified subscription management" + : "🚀 Unlock more features by upgrading your plan"} +

+
+
+
+
+ ); +};