mirror of
http://88.130.71.182:3000/BlitTech/contexta_fe.git
synced 2026-06-12 23:23:22 +00:00
merged with Axel frontend updates
This commit is contained in:
484
package-lock.json
generated
484
package-lock.json
generated
@@ -1,14 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "contexta-fe",
|
"name": "contexta_fe",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "contexta-fe",
|
"name": "contexta_fe",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
|
"@tsparticles/react": "^3.0.0",
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.13.5",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.575.0",
|
"lucide-react": "^0.575.0",
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
"react-dropzone": "^15.0.0",
|
"react-dropzone": "^15.0.0",
|
||||||
"react-router-dom": "^7.13.0",
|
"react-router-dom": "^7.13.0",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
|
"tsparticles-slim": "^2.12.0",
|
||||||
"zustand": "^5.0.11"
|
"zustand": "^5.0.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1455,6 +1457,38 @@
|
|||||||
"react": "^18 || ^19"
|
"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": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"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==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"license": "0BSD"
|
"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": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
|
"@tsparticles/react": "^3.0.0",
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.13.5",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.575.0",
|
"lucide-react": "^0.575.0",
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
"react-dropzone": "^15.0.0",
|
"react-dropzone": "^15.0.0",
|
||||||
"react-router-dom": "^7.13.0",
|
"react-router-dom": "^7.13.0",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
|
"tsparticles-slim": "^2.12.0",
|
||||||
"zustand": "^5.0.11"
|
"zustand": "^5.0.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -32,9 +34,9 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
"typescript": "~5.9.3",
|
|
||||||
"postcss": "^8.5.5",
|
"postcss": "^8.5.5",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.48.0",
|
"typescript-eslint": "^8.48.0",
|
||||||
"vite": "^7.3.1"
|
"vite": "^7.3.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#root {
|
#root {
|
||||||
max-width: 1280px;
|
/* max-width: 1280px; */
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2rem;
|
padding-inline: 1rem;
|
||||||
text-align: center;
|
text-align: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
LogOut, Menu, Sparkles, BarChart3, Mail, Users,
|
LogOut, Menu, Sparkles, BarChart3, Mail, Users,
|
||||||
Shield, X, CalendarDays, Megaphone,
|
Shield, X, CalendarDays, Megaphone,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import { Divider } from './ui'
|
||||||
|
|
||||||
const NAV_ITEMS = [
|
const NAV_ITEMS = [
|
||||||
{ label: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
|
{ label: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
|
||||||
@@ -36,7 +37,7 @@ export const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children })
|
|||||||
const initial = user?.email?.charAt(0).toUpperCase() || '?'
|
const initial = user?.email?.charAt(0).toUpperCase() || '?'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-gray-50 overflow-hidden">
|
<div className="flex h-screen bg-gray-50 overflow-hidden shadow-2xl">
|
||||||
{/* Mobile backdrop */}
|
{/* Mobile backdrop */}
|
||||||
{sidebarOpen && (
|
{sidebarOpen && (
|
||||||
<div
|
<div
|
||||||
@@ -69,7 +70,7 @@ export const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children })
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nav */}
|
{/* Nav */}
|
||||||
<nav className="flex-1 px-3 py-3 space-y-0.5 overflow-y-auto">
|
<nav className="flex-1 px-3 py-3 space-y-4 overflow-y-auto">
|
||||||
{NAV_ITEMS.map(({ label, href, icon: Icon }) => {
|
{NAV_ITEMS.map(({ label, href, icon: Icon }) => {
|
||||||
const active = location.pathname.startsWith(href)
|
const active = location.pathname.startsWith(href)
|
||||||
return (
|
return (
|
||||||
@@ -78,7 +79,7 @@ export const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children })
|
|||||||
to={href}
|
to={href}
|
||||||
onClick={() => setSidebarOpen(false)}
|
onClick={() => setSidebarOpen(false)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group flex items-center gap-3 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',
|
'transition-all duration-150',
|
||||||
active
|
active
|
||||||
? 'bg-primary-50 text-primary-700'
|
? 'bg-primary-50 text-primary-700'
|
||||||
@@ -86,7 +87,7 @@ export const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children })
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className={cn(
|
<Icon className={cn(
|
||||||
'w-4 h-4 shrink-0 transition-transform duration-150',
|
'w-5 h-5 shrink-0 transition-transform duration-150',
|
||||||
active ? 'text-primary-600' : 'text-gray-400 group-hover:text-gray-600',
|
active ? 'text-primary-600' : 'text-gray-400 group-hover:text-gray-600',
|
||||||
)} />
|
)} />
|
||||||
<span className="flex-1">{label}</span>
|
<span className="flex-1">{label}</span>
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ export const AnalyticsPage: React.FC = () => {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="p-4 sm:p-6 max-w-5xl mx-auto space-y-6">
|
<div className="p-4 sm:p-6 max-w-8xl mx-auto space-y-6">
|
||||||
{/* Header skeleton */}
|
{/* Header skeleton */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -476,7 +476,7 @@ export const AnalyticsPage: React.FC = () => {
|
|||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6 max-w-4xl mx-auto">
|
<div className="p-6 max-w-8xl mx-auto">
|
||||||
<Card className="p-10 text-center">
|
<Card className="p-10 text-center">
|
||||||
<div className="w-14 h-14 rounded-2xl bg-gray-50 flex items-center justify-center mx-auto mb-4">
|
<div className="w-14 h-14 rounded-2xl bg-gray-50 flex items-center justify-center mx-auto mb-4">
|
||||||
<BarChart3 className="w-6 h-6 text-gray-400" />
|
<BarChart3 className="w-6 h-6 text-gray-400" />
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ export const AppointmentsPage: React.FC = () => {
|
|||||||
const bookingEnabledChatbots = chatbots.filter(c => c.booking_enabled)
|
const bookingEnabledChatbots = chatbots.filter(c => c.booking_enabled)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 sm:p-6 max-w-5xl mx-auto space-y-6">
|
<div className="p-4 sm:p-6 max-w-8xl mx-auto space-y-6 text-start">
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ const IconInput: React.FC<{
|
|||||||
rightElement?: React.ReactNode
|
rightElement?: React.ReactNode
|
||||||
}> = ({ label, icon, rightElement, ...inputProps }) => (
|
}> = ({ label, icon, rightElement, ...inputProps }) => (
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-sm font-medium text-gray-700">{label}</label>
|
<label className="text-sm text-left font-medium text-gray-700">{label}</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none">
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none">
|
||||||
{icon}
|
{icon}
|
||||||
|
|||||||
@@ -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)
|
const sentTotal = campaigns.filter(c => c.status === 'sent').reduce((sum, c) => sum + c.sent_count, 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 sm:p-6 max-w-3xl mx-auto space-y-6">
|
<div className="p-4 sm:p-6 max-w-8xl mx-auto space-y-6 text-start">
|
||||||
|
|
||||||
{/* Confirm send modal */}
|
{/* Confirm send modal */}
|
||||||
{confirmSendId && (() => {
|
{confirmSendId && (() => {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -57,11 +57,11 @@ export const DashboardPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 sm:p-6 max-w-7xl">
|
<div className="text-start p-4 sm:p-10 max-w-8xl">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-6 gap-3">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-6 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl sm:text-2xl font-bold text-gray-900 tracking-tight">Dashboard</h1>
|
<h1 className="text-xl sm:text-4xl font-bold text-gray-900 tracking-tight">Dashboard</h1>
|
||||||
<p className="text-sm text-gray-500 mt-0.5">
|
<p className="text-sm text-gray-500 mt-0.5">
|
||||||
{chatbots.length > 0
|
{chatbots.length > 0
|
||||||
? `${chatbots.length} chatbot${chatbots.length !== 1 ? 's' : ''}`
|
? `${chatbots.length} chatbot${chatbots.length !== 1 ? 's' : ''}`
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export const ForgotPasswordPage: React.FC = () => {
|
|||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
{/* Email input with icon */}
|
{/* Email input with icon */}
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-sm font-medium text-gray-700">Email address</label>
|
<label className="text-sm text-left font-medium text-gray-700">Email address</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none">
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none">
|
||||||
<Mail className="w-4 h-4" />
|
<Mail className="w-4 h-4" />
|
||||||
|
|||||||
@@ -137,8 +137,7 @@ export const LeadsPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 sm:p-6 max-w-5xl mx-auto space-y-6">
|
<div className="p-4 sm:p-6 max-w-8xl mx-auto space-y-6 text-start">
|
||||||
|
|
||||||
{notesLead && (
|
{notesLead && (
|
||||||
<NotesModal
|
<NotesModal
|
||||||
lead={notesLead}
|
lead={notesLead}
|
||||||
|
|||||||
@@ -1,51 +1,67 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from "react";
|
||||||
import { useParams, useNavigate } from 'react-router-dom'
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { marketplaceAPI } from '@/services/api'
|
import { marketplaceAPI } from "@/services/api";
|
||||||
import { Spinner, EmptyState, Button } from '@/components/ui'
|
import { Spinner, EmptyState, Button, Card } from "@/components/ui";
|
||||||
import { SkeletonCard } from '@/components/Skeletons'
|
import { SkeletonCard } from "@/components/Skeletons";
|
||||||
import { ChatInterface } from '@/components/ChatInterface'
|
import { ChatInterface } from "@/components/ChatInterface";
|
||||||
import { CATEGORIES, INDUSTRIES } from '@/lib/utils'
|
import { CATEGORIES, INDUSTRIES } from "@/lib/utils";
|
||||||
import { Search, Bot, Star, MessageSquare, ArrowLeft, SlidersHorizontal, ChevronLeft, ChevronRight } from 'lucide-react'
|
import {
|
||||||
import type { ChatbotPublic } from '@/types'
|
Search,
|
||||||
|
Bot,
|
||||||
|
Star,
|
||||||
|
MessageSquare,
|
||||||
|
ArrowLeft,
|
||||||
|
SlidersHorizontal,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
} from "lucide-react";
|
||||||
|
import type { ChatbotPublic } from "@/types";
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
// MARKETPLACE LISTING PAGE
|
// MARKETPLACE LISTING PAGE
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
export const MarketplacePage: React.FC = () => {
|
export const MarketplacePage: React.FC = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate();
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState("");
|
||||||
const [debouncedSearch, setDebouncedSearch] = useState('')
|
const [debouncedSearch, setDebouncedSearch] = useState("");
|
||||||
const [category, setCategory] = useState('')
|
const [category, setCategory] = useState("");
|
||||||
const [industry, setIndustry] = useState('')
|
const [industry, setIndustry] = useState("");
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1);
|
||||||
const [showFilters, setShowFilters] = useState(false)
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
|
|
||||||
// Debounce search
|
// Debounce search
|
||||||
const searchTimeout = React.useRef<ReturnType<typeof setTimeout>>()
|
const searchTimeout = React.useRef<ReturnType<typeof setTimeout>>();
|
||||||
const handleSearch = (value: string) => {
|
const handleSearch = (value: string) => {
|
||||||
setSearch(value)
|
setSearch(value);
|
||||||
clearTimeout(searchTimeout.current)
|
clearTimeout(searchTimeout.current);
|
||||||
searchTimeout.current = setTimeout(() => {
|
searchTimeout.current = setTimeout(() => {
|
||||||
setDebouncedSearch(value)
|
setDebouncedSearch(value);
|
||||||
setPage(1)
|
setPage(1);
|
||||||
}, 300)
|
}, 300);
|
||||||
}
|
};
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: ['marketplace', debouncedSearch, category, industry, page],
|
queryKey: ["marketplace", debouncedSearch, category, industry, page],
|
||||||
queryFn: () => marketplaceAPI.list({ search: debouncedSearch, category, industry, page, limit: 20 }),
|
queryFn: () =>
|
||||||
})
|
marketplaceAPI.list({
|
||||||
|
search: debouncedSearch,
|
||||||
|
category,
|
||||||
|
industry,
|
||||||
|
page,
|
||||||
|
limit: 20,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
const totalPages = data ? Math.ceil(data.total / 20) : 0
|
const totalPages = data ? Math.ceil(data.total / 20) : 0;
|
||||||
const hasActiveFilters = category !== '' || industry !== ''
|
const hasActiveFilters = category !== "" || industry !== "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-full bg-gray-50/50">
|
<div className="min-h-full bg-gray-50/50 ">
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
<div className="bg-white border-b border-gray-200">
|
<div className="bg-white border-b border-gray-200 ">
|
||||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8 sm:py-10">
|
<div className="max-w-8xl mx-auto px-4 sm:px-6 py-8 sm:py-10">
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center shadow-sm">
|
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center shadow-sm">
|
||||||
@@ -56,13 +72,15 @@ export const MarketplacePage: React.FC = () => {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-500 text-sm sm:text-base max-w-xl">
|
<p className="text-gray-500 text-sm sm:text-base max-w-xl">
|
||||||
Discover and interact with AI-powered chatbots built by businesses — ready to answer your questions instantly.
|
Discover and interact with AI-powered chatbots built by businesses
|
||||||
|
— ready to answer your questions instantly.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-6">
|
{/* Content */}
|
||||||
|
<div className="max-w-8xl mx-auto px-4 sm:px-6 py-6">
|
||||||
{/* Search & Filter Bar */}
|
{/* Search & Filter Bar */}
|
||||||
<div className="mb-6 animate-fade-in-down space-y-3">
|
<div className="mb-6 animate-fade-in-down space-y-3">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -71,17 +89,27 @@ export const MarketplacePage: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={e => handleSearch(e.target.value)}
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
placeholder="Search chatbots by name or description..."
|
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"
|
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 && (
|
{search && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleSearch('')}
|
onClick={() => handleSearch("")}
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
className="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -90,8 +118,8 @@ export const MarketplacePage: React.FC = () => {
|
|||||||
onClick={() => setShowFilters(!showFilters)}
|
onClick={() => setShowFilters(!showFilters)}
|
||||||
className={`flex items-center gap-2 px-4 py-2.5 rounded-xl border text-sm font-medium transition-all shadow-sm ${
|
className={`flex items-center gap-2 px-4 py-2.5 rounded-xl border text-sm font-medium transition-all shadow-sm ${
|
||||||
showFilters || hasActiveFilters
|
showFilters || hasActiveFilters
|
||||||
? 'bg-primary-50 border-primary-300 text-primary-700'
|
? "bg-primary-50 border-primary-300 text-primary-700"
|
||||||
: 'bg-white border-gray-200 text-gray-600 hover:bg-gray-50'
|
: "bg-white border-gray-200 text-gray-600 hover:bg-gray-50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<SlidersHorizontal className="w-4 h-4" />
|
<SlidersHorizontal className="w-4 h-4" />
|
||||||
@@ -109,26 +137,34 @@ export const MarketplacePage: React.FC = () => {
|
|||||||
<div className="bg-white border border-gray-200 rounded-xl p-4 shadow-sm space-y-4 animate-fade-in-down">
|
<div className="bg-white border border-gray-200 rounded-xl p-4 shadow-sm space-y-4 animate-fade-in-down">
|
||||||
{/* Category filter — pill buttons since list is manageable */}
|
{/* Category filter — pill buttons since list is manageable */}
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2.5">Category</p>
|
<p className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2.5">
|
||||||
|
Category
|
||||||
|
</p>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => { setCategory(''); setPage(1) }}
|
onClick={() => {
|
||||||
|
setCategory("");
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
className={`px-3 py-1.5 rounded-full text-xs font-medium transition-all border ${
|
className={`px-3 py-1.5 rounded-full text-xs font-medium transition-all border ${
|
||||||
category === ''
|
category === ""
|
||||||
? 'bg-primary-600 text-white border-primary-600 shadow-sm'
|
? "bg-primary-600 text-white border-primary-600 shadow-sm"
|
||||||
: 'bg-white text-gray-600 border-gray-200 hover:border-primary-300 hover:text-primary-600'
|
: "bg-white text-gray-600 border-gray-200 hover:border-primary-300 hover:text-primary-600"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
All
|
All
|
||||||
</button>
|
</button>
|
||||||
{CATEGORIES.map(c => (
|
{CATEGORIES.map((c) => (
|
||||||
<button
|
<button
|
||||||
key={c}
|
key={c}
|
||||||
onClick={() => { setCategory(category === c ? '' : c); setPage(1) }}
|
onClick={() => {
|
||||||
|
setCategory(category === c ? "" : c);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
className={`px-3 py-1.5 rounded-full text-xs font-medium transition-all border ${
|
className={`px-3 py-1.5 rounded-full text-xs font-medium transition-all border ${
|
||||||
category === c
|
category === c
|
||||||
? 'bg-primary-600 text-white border-primary-600 shadow-sm'
|
? "bg-primary-600 text-white border-primary-600 shadow-sm"
|
||||||
: 'bg-white text-gray-600 border-gray-200 hover:border-primary-300 hover:text-primary-600'
|
: "bg-white text-gray-600 border-gray-200 hover:border-primary-300 hover:text-primary-600"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{c}
|
{c}
|
||||||
@@ -139,20 +175,33 @@ export const MarketplacePage: React.FC = () => {
|
|||||||
|
|
||||||
{/* Industry filter — select dropdown since list is long */}
|
{/* Industry filter — select dropdown since list is long */}
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2.5">Industry</p>
|
<p className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2.5">
|
||||||
|
Industry
|
||||||
|
</p>
|
||||||
<select
|
<select
|
||||||
value={industry}
|
value={industry}
|
||||||
onChange={e => { setIndustry(e.target.value); setPage(1) }}
|
onChange={(e) => {
|
||||||
|
setIndustry(e.target.value);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
className="w-full sm:w-72 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 bg-white transition-all text-gray-700"
|
className="w-full sm:w-72 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 bg-white transition-all text-gray-700"
|
||||||
>
|
>
|
||||||
<option value="">All Industries</option>
|
<option value="">All Industries</option>
|
||||||
{INDUSTRIES.map(i => <option key={i} value={i}>{i}</option>)}
|
{INDUSTRIES.map((i) => (
|
||||||
|
<option key={i} value={i}>
|
||||||
|
{i}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasActiveFilters && (
|
{hasActiveFilters && (
|
||||||
<button
|
<button
|
||||||
onClick={() => { setCategory(''); setIndustry(''); setPage(1) }}
|
onClick={() => {
|
||||||
|
setCategory("");
|
||||||
|
setIndustry("");
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
className="text-xs text-red-500 hover:text-red-700 transition-colors font-medium"
|
className="text-xs text-red-500 hover:text-red-700 transition-colors font-medium"
|
||||||
>
|
>
|
||||||
Clear all filters
|
Clear all filters
|
||||||
@@ -180,11 +229,20 @@ export const MarketplacePage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
action={
|
action={
|
||||||
hasActiveFilters || debouncedSearch ? (
|
hasActiveFilters || debouncedSearch ? (
|
||||||
<Button variant="outline" onClick={() => { setCategory(''); setIndustry(''); handleSearch('') }}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setCategory("");
|
||||||
|
setIndustry("");
|
||||||
|
handleSearch("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
Clear filters
|
Clear filters
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button onClick={() => navigate('/chatbots/new')}>Create Chatbot</Button>
|
<Button onClick={() => navigate("/chatbots/new")}>
|
||||||
|
Create Chatbot
|
||||||
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -192,11 +250,15 @@ export const MarketplacePage: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<p className="text-xs text-gray-400 font-medium uppercase tracking-wide">
|
<p className="text-xs text-gray-400 font-medium uppercase tracking-wide">
|
||||||
{data.total} chatbot{data.total !== 1 ? 's' : ''} available
|
{data.total} chatbot{data.total !== 1 ? "s" : ""} available
|
||||||
</p>
|
</p>
|
||||||
{hasActiveFilters && (
|
{hasActiveFilters && (
|
||||||
<button
|
<button
|
||||||
onClick={() => { setCategory(''); setIndustry(''); setPage(1) }}
|
onClick={() => {
|
||||||
|
setCategory("");
|
||||||
|
setIndustry("");
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
className="text-xs text-primary-600 hover:text-primary-800 transition-colors"
|
className="text-xs text-primary-600 hover:text-primary-800 transition-colors"
|
||||||
>
|
>
|
||||||
Clear filters
|
Clear filters
|
||||||
@@ -219,7 +281,7 @@ export const MarketplacePage: React.FC = () => {
|
|||||||
{data.total > 20 && (
|
{data.total > 20 && (
|
||||||
<div className="flex justify-center items-center gap-2">
|
<div className="flex justify-center items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage(p => Math.max(1, p - 1))}
|
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||||
disabled={page === 1}
|
disabled={page === 1}
|
||||||
className="w-9 h-9 flex items-center justify-center rounded-lg border border-gray-200 bg-white text-gray-600 hover:bg-gray-50 hover:border-gray-300 disabled:opacity-40 disabled:cursor-not-allowed transition-all shadow-sm"
|
className="w-9 h-9 flex items-center justify-center rounded-lg border border-gray-200 bg-white text-gray-600 hover:bg-gray-50 hover:border-gray-300 disabled:opacity-40 disabled:cursor-not-allowed transition-all shadow-sm"
|
||||||
>
|
>
|
||||||
@@ -227,15 +289,22 @@ export const MarketplacePage: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{Array.from({ length: totalPages }, (_, i) => i + 1)
|
{Array.from({ length: totalPages }, (_, i) => i + 1)
|
||||||
.filter(p => p === 1 || p === totalPages || Math.abs(p - page) <= 1)
|
.filter(
|
||||||
.reduce<(number | 'ellipsis')[]>((acc, p, idx, arr) => {
|
(p) =>
|
||||||
if (idx > 0 && p - (arr[idx - 1] as number) > 1) acc.push('ellipsis')
|
p === 1 || p === totalPages || Math.abs(p - page) <= 1,
|
||||||
acc.push(p)
|
)
|
||||||
return acc
|
.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) =>
|
.map((p, idx) =>
|
||||||
p === 'ellipsis' ? (
|
p === "ellipsis" ? (
|
||||||
<span key={`ellipsis-${idx}`} className="w-9 h-9 flex items-center justify-center text-gray-400 text-sm">
|
<span
|
||||||
|
key={`ellipsis-${idx}`}
|
||||||
|
className="w-9 h-9 flex items-center justify-center text-gray-400 text-sm"
|
||||||
|
>
|
||||||
…
|
…
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -244,17 +313,17 @@ export const MarketplacePage: React.FC = () => {
|
|||||||
onClick={() => setPage(p as number)}
|
onClick={() => setPage(p as number)}
|
||||||
className={`w-9 h-9 flex items-center justify-center rounded-lg text-sm font-medium transition-all ${
|
className={`w-9 h-9 flex items-center justify-center rounded-lg text-sm font-medium transition-all ${
|
||||||
page === p
|
page === p
|
||||||
? 'bg-primary-600 text-white shadow-sm shadow-primary-200'
|
? "bg-primary-600 text-white shadow-sm shadow-primary-200"
|
||||||
: 'border border-gray-200 bg-white text-gray-600 hover:bg-gray-50 hover:border-gray-300'
|
: "border border-gray-200 bg-white text-gray-600 hover:bg-gray-50 hover:border-gray-300"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{p}
|
{p}
|
||||||
</button>
|
</button>
|
||||||
)
|
),
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage(p => p + 1)}
|
onClick={() => setPage((p) => p + 1)}
|
||||||
disabled={!data.has_more}
|
disabled={!data.has_more}
|
||||||
className="w-9 h-9 flex items-center justify-center rounded-lg border border-gray-200 bg-white text-gray-600 hover:bg-gray-50 hover:border-gray-300 disabled:opacity-40 disabled:cursor-not-allowed transition-all shadow-sm"
|
className="w-9 h-9 flex items-center justify-center rounded-lg border border-gray-200 bg-white text-gray-600 hover:bg-gray-50 hover:border-gray-300 disabled:opacity-40 disabled:cursor-not-allowed transition-all shadow-sm"
|
||||||
>
|
>
|
||||||
@@ -266,24 +335,29 @@ export const MarketplacePage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
// MARKETPLACE CARD — shows logo when available
|
// 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 }) => (
|
||||||
<div
|
<div
|
||||||
className="group relative bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-lg hover:border-gray-300 hover:-translate-y-1 transition-all duration-200 cursor-pointer overflow-hidden"
|
className="group relative bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-lg hover:border-gray-300 hover:-translate-y-1 transition-all duration-200 cursor-pointer overflow-hidden"
|
||||||
style={{ animationDelay: `${index * 60}ms`, animationFillMode: 'both' }}
|
style={{ animationDelay: `${index * 60}ms`, animationFillMode: "both" }}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{/* Colored accent top bar — thicker and with gradient */}
|
{/* Colored accent top bar — thicker and with gradient */}
|
||||||
<div
|
<div
|
||||||
className="h-1.5 w-full"
|
className="h-1.5 w-full"
|
||||||
style={{ background: `linear-gradient(90deg, ${chatbot.primary_color}, ${chatbot.primary_color}aa)` }}
|
style={{
|
||||||
|
background: `linear-gradient(90deg, ${chatbot.primary_color}, ${chatbot.primary_color}aa)`,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="p-5">
|
<div className="p-5">
|
||||||
@@ -308,14 +382,18 @@ const ChatbotMarketplaceCard: React.FC<{ chatbot: ChatbotPublic; onClick: () =>
|
|||||||
{chatbot.name}
|
{chatbot.name}
|
||||||
</h3>
|
</h3>
|
||||||
{chatbot.company_name && (
|
{chatbot.company_name && (
|
||||||
<p className="text-xs text-gray-400 truncate mt-0.5">by {chatbot.company_name}</p>
|
<p className="text-xs text-gray-400 truncate mt-0.5">
|
||||||
|
by {chatbot.company_name}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
{chatbot.description ? (
|
{chatbot.description ? (
|
||||||
<p className="text-xs text-gray-500 mb-4 line-clamp-2 leading-relaxed">{chatbot.description}</p>
|
<p className="text-xs text-gray-500 mb-4 line-clamp-2 leading-relaxed">
|
||||||
|
{chatbot.description}
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="mb-4" />
|
<div className="mb-4" />
|
||||||
)}
|
)}
|
||||||
@@ -341,7 +419,7 @@ const ChatbotMarketplaceCard: React.FC<{ chatbot: ChatbotPublic; onClick: () =>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hover overlay: "Chat now" CTA */}
|
{/* Hover overlay: "Chat now" CTA */}
|
||||||
<div className="absolute inset-0 flex items-end justify-center pb-5 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none">
|
<div className="absolute inset-0 flex items-end justify-end mr-5 pb-5 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none">
|
||||||
<div
|
<div
|
||||||
className="px-5 py-2 rounded-full text-white text-xs font-semibold shadow-lg pointer-events-none"
|
className="px-5 py-2 rounded-full text-white text-xs font-semibold shadow-lg pointer-events-none"
|
||||||
style={{ background: chatbot.primary_color }}
|
style={{ background: chatbot.primary_color }}
|
||||||
@@ -350,29 +428,32 @@ const ChatbotMarketplaceCard: React.FC<{ chatbot: ChatbotPublic; onClick: () =>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
// CHATBOT DETAIL PAGE — shows logo in header + passes to ChatInterface
|
// CHATBOT DETAIL PAGE — shows logo in header + passes to ChatInterface
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
export const ChatbotDetailPage: React.FC = () => {
|
export const ChatbotDetailPage: React.FC = () => {
|
||||||
const { id } = useParams<{ id: string }>()
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { data: chatbot, isLoading, error } = useQuery({
|
const {
|
||||||
queryKey: ['marketplace-chatbot', id],
|
data: chatbot,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ["marketplace-chatbot", id],
|
||||||
queryFn: () => marketplaceAPI.get(id!),
|
queryFn: () => marketplaceAPI.get(id!),
|
||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
})
|
});
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center py-20">
|
<div className="flex justify-center py-20">
|
||||||
<Spinner className="text-primary-600" />
|
<Spinner className="text-primary-600" />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error || !chatbot) {
|
if (error || !chatbot) {
|
||||||
@@ -383,21 +464,21 @@ export const ChatbotDetailPage: React.FC = () => {
|
|||||||
title="Chatbot not found"
|
title="Chatbot not found"
|
||||||
description="This chatbot may have been unpublished or removed."
|
description="This chatbot may have been unpublished or removed."
|
||||||
action={
|
action={
|
||||||
<Button onClick={() => navigate('/marketplace')} variant="outline">
|
<Button onClick={() => navigate("/marketplace")} variant="outline">
|
||||||
<ArrowLeft className="w-4 h-4" />
|
<ArrowLeft className="w-4 h-4" />
|
||||||
Back to Marketplace
|
Back to Marketplace
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 sm:p-6 max-w-4xl mx-auto animate-fade-in">
|
<Card className="p-4 sm:p-6 max-w-5xl mx-auto animate-fade-in">
|
||||||
{/* Back link */}
|
{/* Back link */}
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/marketplace')}
|
onClick={() => navigate("/marketplace")}
|
||||||
className="flex items-center gap-1.5 text-sm text-gray-500 hover:text-primary-600 mb-6 transition-colors group"
|
className="flex items-center gap-1.5 text-sm text-gray-500 hover:text-primary-600 mb-6 transition-colors group"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-0.5 transition-transform" />
|
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-0.5 transition-transform" />
|
||||||
@@ -409,7 +490,9 @@ export const ChatbotDetailPage: React.FC = () => {
|
|||||||
{/* Accent bar */}
|
{/* Accent bar */}
|
||||||
<div
|
<div
|
||||||
className="h-1.5 w-full"
|
className="h-1.5 w-full"
|
||||||
style={{ background: `linear-gradient(90deg, ${chatbot.primary_color}, ${chatbot.primary_color}88)` }}
|
style={{
|
||||||
|
background: `linear-gradient(90deg, ${chatbot.primary_color}, ${chatbot.primary_color}88)`,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="p-5 sm:p-6">
|
<div className="p-5 sm:p-6">
|
||||||
<div className="flex items-center gap-4 mb-3">
|
<div className="flex items-center gap-4 mb-3">
|
||||||
@@ -428,9 +511,13 @@ export const ChatbotDetailPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl sm:text-2xl font-bold text-gray-900">{chatbot.name}</h1>
|
<h1 className="text-xl sm:text-2xl font-bold text-gray-900">
|
||||||
|
{chatbot.name}
|
||||||
|
</h1>
|
||||||
{chatbot.company_name && (
|
{chatbot.company_name && (
|
||||||
<p className="text-sm text-gray-500">by {chatbot.company_name}</p>
|
<p className="text-sm text-gray-500">
|
||||||
|
by {chatbot.company_name}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
|
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
|
||||||
{chatbot.average_rating && chatbot.average_rating > 0 && (
|
{chatbot.average_rating && chatbot.average_rating > 0 && (
|
||||||
@@ -453,7 +540,9 @@ export const ChatbotDetailPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{chatbot.description && (
|
{chatbot.description && (
|
||||||
<p className="text-gray-500 text-sm leading-relaxed">{chatbot.description}</p>
|
<p className="text-gray-500 text-sm leading-relaxed">
|
||||||
|
{chatbot.description}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -468,6 +557,6 @@ export const ChatbotDetailPage: React.FC = () => {
|
|||||||
logoUrl={chatbot.logo_url}
|
logoUrl={chatbot.logo_url}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,56 +1,67 @@
|
|||||||
import React, { useState, useCallback } from 'react'
|
import React, { useState, useCallback } from "react";
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useNavigate, useLocation, Link } from 'react-router-dom'
|
import { useNavigate, useLocation, Link } from "react-router-dom";
|
||||||
import { billingAPI, authAPI } from '@/services/api'
|
import { billingAPI, authAPI } from "@/services/api";
|
||||||
import { useAuthStore } from '@/store/authStore'
|
import { useAuthStore } from "@/store/authStore";
|
||||||
import { Button, Card, Input } from '@/components/ui'
|
import { Button, Card, Input } from "@/components/ui";
|
||||||
import { useToast } from '@/contexts/ToastContext'
|
import { useToast } from "@/contexts/ToastContext";
|
||||||
import { useThemeStore } from '@/store/themeStore'
|
import { useThemeStore } from "@/store/themeStore";
|
||||||
import { getPlanColor, formatDate } from '@/lib/utils'
|
import { getPlanColor, formatDate } from "@/lib/utils";
|
||||||
import { CreditCard, User, ExternalLink, AlertTriangle, Moon, Sun } from 'lucide-react'
|
import {
|
||||||
|
CreditCard,
|
||||||
|
User,
|
||||||
|
ExternalLink,
|
||||||
|
AlertTriangle,
|
||||||
|
Moon,
|
||||||
|
Sun,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
export const SettingsPage: React.FC = () => {
|
export const SettingsPage: React.FC = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate();
|
||||||
const location = useLocation()
|
const location = useLocation();
|
||||||
const { success: showToast, error: showError } = useToast()
|
const { success: showToast, error: showError } = useToast();
|
||||||
const { isDark, toggle: toggleTheme } = useThemeStore()
|
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 handleTabChange = useCallback(
|
||||||
const newPath = newTab === 'billing' ? '/settings/billing' : '/settings'
|
(newTab: "profile" | "billing") => {
|
||||||
|
const newPath = newTab === "billing" ? "/settings/billing" : "/settings";
|
||||||
if (location.pathname !== newPath) {
|
if (location.pathname !== newPath) {
|
||||||
navigate(newPath, { replace: true })
|
navigate(newPath, { replace: true });
|
||||||
}
|
}
|
||||||
}, [navigate, location.pathname])
|
},
|
||||||
|
[navigate, location.pathname],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 max-w-3xl mx-auto">
|
<div className="p-6 max-w-8xl mx-auto">
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Settings</h1>
|
<h1 className="text-4xl font-bold text-gray-900">Settings</h1>
|
||||||
<button
|
<button
|
||||||
onClick={toggleTheme}
|
onClick={toggleTheme}
|
||||||
className="flex items-center gap-2 px-3 py-2 rounded-lg border border-gray-200 hover:bg-gray-100 text-sm text-gray-600 transition-colors"
|
className="flex items-center gap-2 px-3 py-2 rounded-lg border border-gray-200 hover:bg-gray-100 text-sm text-gray-600 transition-colors"
|
||||||
aria-label="Toggle dark mode"
|
aria-label="Toggle dark mode"
|
||||||
>
|
>
|
||||||
{isDark ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
|
{isDark ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
|
||||||
{isDark ? 'Light mode' : 'Dark mode'}
|
{isDark ? "Light mode" : "Dark mode"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="flex gap-1 mb-6 bg-gray-100 p-1 rounded-xl w-fit">
|
<div className="flex gap-1 mb-6 bg-gray-100 p-1 rounded-xl w-fit">
|
||||||
{[
|
{[
|
||||||
{ id: 'profile' as const, label: 'Profile', icon: User },
|
{ id: "profile" as const, label: "Profile", icon: User },
|
||||||
{ id: 'billing' as const, label: 'Billing', icon: CreditCard },
|
{ id: "billing" as const, label: "Billing", icon: CreditCard },
|
||||||
].map(({ id, label, icon: Icon }) => (
|
].map(({ id, label, icon: Icon }) => (
|
||||||
<button
|
<button
|
||||||
key={id}
|
key={id}
|
||||||
onClick={() => handleTabChange(id)}
|
onClick={() => handleTabChange(id)}
|
||||||
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
className={`flex lg:w-52 items-center justify-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
tab === id
|
tab === id
|
||||||
? 'bg-white shadow-sm text-gray-900'
|
? "bg-white shadow-sm text-gray-900"
|
||||||
: 'text-gray-500 hover:text-gray-700'
|
: "text-gray-500 hover:text-gray-700"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon className="w-3.5 h-3.5" />
|
<Icon className="w-3.5 h-3.5" />
|
||||||
@@ -59,81 +70,105 @@ export const SettingsPage: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tab === 'profile' && <ProfileSettings onToast={showToast} onError={showError} />}
|
{tab === "profile" && (
|
||||||
{tab === 'billing' && <BillingSettings onToast={showToast} onError={showError} />}
|
<ProfileSettings onToast={showToast} onError={showError} />
|
||||||
|
)}
|
||||||
|
{tab === "billing" && (
|
||||||
|
<BillingSettings onToast={showToast} onError={showError} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const ProfileSettings: React.FC<{ onToast: (msg: string) => void; onError: (msg: string) => void }> = ({ onToast, onError }) => {
|
const ProfileSettings: React.FC<{
|
||||||
const { user, setAuth, token, logout } = useAuthStore()
|
onToast: (msg: string) => void;
|
||||||
const navigate = useNavigate()
|
onError: (msg: string) => void;
|
||||||
const [companyName, setCompanyName] = useState(user?.company_name || '')
|
}> = ({ onToast, onError }) => {
|
||||||
const [currentPassword, setCurrentPassword] = useState('')
|
const { user, setAuth, token, logout } = useAuthStore();
|
||||||
const [newPassword, setNewPassword] = useState('')
|
const navigate = useNavigate();
|
||||||
const [saving, setSaving] = useState(false)
|
const [companyName, setCompanyName] = useState(user?.company_name || "");
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
const [currentPassword, setCurrentPassword] = useState("");
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState('')
|
const [newPassword, setNewPassword] = useState("");
|
||||||
const [deleting, setDeleting] = useState(false)
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
const [deleteConfirm, setDeleteConfirm] = useState("");
|
||||||
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaving(true)
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const payload: { company_name?: string; current_password?: string; new_password?: string } = {}
|
const payload: {
|
||||||
if (companyName !== user?.company_name) payload.company_name = companyName
|
company_name?: string;
|
||||||
|
current_password?: string;
|
||||||
|
new_password?: string;
|
||||||
|
} = {};
|
||||||
|
if (companyName !== user?.company_name)
|
||||||
|
payload.company_name = companyName;
|
||||||
if (newPassword) {
|
if (newPassword) {
|
||||||
payload.current_password = currentPassword
|
payload.current_password = currentPassword;
|
||||||
payload.new_password = newPassword
|
payload.new_password = newPassword;
|
||||||
}
|
}
|
||||||
if (Object.keys(payload).length === 0) {
|
if (Object.keys(payload).length === 0) {
|
||||||
onToast('No changes to save')
|
onToast("No changes to save");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
const updated = await authAPI.updateProfile(payload)
|
const updated = await authAPI.updateProfile(payload);
|
||||||
setAuth(updated, token || '')
|
setAuth(updated, token || "");
|
||||||
setCurrentPassword('')
|
setCurrentPassword("");
|
||||||
setNewPassword('')
|
setNewPassword("");
|
||||||
onToast('Profile updated successfully')
|
onToast("Profile updated successfully");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const e = err as { response?: { data?: { detail?: string } } }
|
const e = err as { response?: { data?: { detail?: string } } };
|
||||||
onError(e.response?.data?.detail || 'Failed to update profile')
|
onError(e.response?.data?.detail || "Failed to update profile");
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false)
|
setSaving(false);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteAccount = async () => {
|
const handleDeleteAccount = async () => {
|
||||||
if (deleteConfirm !== 'DELETE') return
|
if (deleteConfirm !== "DELETE") return;
|
||||||
setDeleting(true)
|
setDeleting(true);
|
||||||
try {
|
try {
|
||||||
await authAPI.deleteAccount()
|
await authAPI.deleteAccount();
|
||||||
logout()
|
logout();
|
||||||
navigate('/')
|
navigate("/");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const e = err as { response?: { data?: { detail?: string } } }
|
const e = err as { response?: { data?: { detail?: string } } };
|
||||||
onError(e.response?.data?.detail || 'Failed to delete account')
|
onError(e.response?.data?.detail || "Failed to delete account");
|
||||||
setDeleting(false)
|
setDeleting(false);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Card className="p-6 space-y-4">
|
<Card className="p-6 space-y-4">
|
||||||
<h2 className="font-semibold text-gray-900">Profile Information</h2>
|
<h2 className="font-semibold text-gray-900">Profile Information</h2>
|
||||||
<Input label="Email" value={user?.email || ''} disabled hint="Email cannot be changed" />
|
<Input
|
||||||
|
label="Email"
|
||||||
|
value={user?.email || ""}
|
||||||
|
disabled
|
||||||
|
hint="Email cannot be changed"
|
||||||
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="Company Name"
|
label="Company Name"
|
||||||
value={companyName}
|
value={companyName}
|
||||||
onChange={e => setCompanyName(e.target.value)}
|
onChange={(e) => setCompanyName(e.target.value)}
|
||||||
placeholder="Your company name"
|
placeholder="Your company name"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1.5">Plan</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||||||
|
Plan
|
||||||
|
</label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className={`px-3 py-1 text-sm font-medium rounded-full capitalize ${getPlanColor(user?.plan || 'free')}`}>
|
<span
|
||||||
{user?.plan || 'free'}
|
className={`px-3 py-1 text-sm font-medium rounded-full capitalize ${getPlanColor(user?.plan || "free")}`}
|
||||||
|
>
|
||||||
|
{user?.plan || "free"}
|
||||||
</span>
|
</span>
|
||||||
<Link to="/pricing" className="text-sm text-primary-600 hover:underline">
|
<Link
|
||||||
|
to="/pricing"
|
||||||
|
className="text-sm text-primary-600 hover:underline"
|
||||||
|
>
|
||||||
Manage plan
|
Manage plan
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,33 +181,44 @@ const ProfileSettings: React.FC<{ onToast: (msg: string) => void; onError: (msg:
|
|||||||
label="Current Password"
|
label="Current Password"
|
||||||
type="password"
|
type="password"
|
||||||
value={currentPassword}
|
value={currentPassword}
|
||||||
onChange={e => setCurrentPassword(e.target.value)}
|
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||||
placeholder="Enter current password"
|
placeholder="Enter current password"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="New Password"
|
label="New Password"
|
||||||
type="password"
|
type="password"
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={e => setNewPassword(e.target.value)}
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
placeholder="Min 8 characters"
|
placeholder="Min 8 characters"
|
||||||
hint="Leave blank to keep current password"
|
hint="Leave blank to keep current password"
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Button onClick={handleSave} loading={saving}>
|
<div className="w-full flex items-center justify-center">
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
loading={saving}
|
||||||
|
className="w-1/2 h-11 my-5"
|
||||||
|
>
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Danger Zone */}
|
{/* Danger Zone */}
|
||||||
<Card className="p-6 border-red-200 bg-red-50/30">
|
<Card className="p-6 border-red-200 bg-red-50/30 text-center">
|
||||||
<h2 className="font-semibold text-red-800 mb-2 flex items-center gap-1.5">
|
<h2 className="font-semibold text-lg text-red-800 mb-2 flex items-center justify-center gap-1.5">
|
||||||
<AlertTriangle className="w-4 h-4" />
|
<AlertTriangle className="w-4 h-4" />
|
||||||
Danger Zone
|
Danger Zone
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-red-700 mb-4">
|
<p className="text-sm text-red-700 mb-4">
|
||||||
Permanently delete your account, all chatbots, documents, and data. This cannot be undone.
|
Permanently delete your account, all chatbots, documents, and data.
|
||||||
|
This cannot be undone.
|
||||||
</p>
|
</p>
|
||||||
<Button variant="outline" className="border-red-300 text-red-700 hover:bg-red-50" onClick={() => setShowDeleteModal(true)}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="border-red-300 text-red-700 hover:bg-red-50"
|
||||||
|
onClick={() => setShowDeleteModal(true)}
|
||||||
|
>
|
||||||
Delete Account
|
Delete Account
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -181,24 +227,39 @@ const ProfileSettings: React.FC<{ onToast: (msg: string) => void; onError: (msg:
|
|||||||
{showDeleteModal && (
|
{showDeleteModal && (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
<div className="bg-white rounded-2xl p-6 w-full max-w-md shadow-xl">
|
<div className="bg-white rounded-2xl p-6 w-full max-w-md shadow-xl">
|
||||||
<h3 className="text-lg font-bold text-gray-900 mb-2">Delete Account</h3>
|
<h3 className="text-lg font-bold text-gray-900 mb-2">
|
||||||
|
Delete Account
|
||||||
|
</h3>
|
||||||
<p className="text-sm text-gray-500 mb-4">
|
<p className="text-sm text-gray-500 mb-4">
|
||||||
This will permanently delete your account and all associated data including chatbots, documents, conversations, and leads.
|
This will permanently delete your account and all associated data
|
||||||
<strong className="text-red-600"> This action cannot be undone.</strong>
|
including chatbots, documents, conversations, and leads.
|
||||||
|
<strong className="text-red-600">
|
||||||
|
{" "}
|
||||||
|
This action cannot be undone.
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-700 mb-2">
|
||||||
|
Type <strong>DELETE</strong> to confirm:
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-700 mb-2">Type <strong>DELETE</strong> to confirm:</p>
|
|
||||||
<Input
|
<Input
|
||||||
value={deleteConfirm}
|
value={deleteConfirm}
|
||||||
onChange={e => setDeleteConfirm(e.target.value)}
|
onChange={(e) => setDeleteConfirm(e.target.value)}
|
||||||
placeholder="DELETE"
|
placeholder="DELETE"
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-3 mt-4">
|
<div className="flex gap-3 mt-4">
|
||||||
<Button variant="outline" className="flex-1" onClick={() => { setShowDeleteModal(false); setDeleteConfirm('') }}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={() => {
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
setDeleteConfirm("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="flex-1 bg-red-600 hover:bg-red-700"
|
className="flex-1 bg-red-600 hover:bg-red-700"
|
||||||
disabled={deleteConfirm !== 'DELETE'}
|
disabled={deleteConfirm !== "DELETE"}
|
||||||
loading={deleting}
|
loading={deleting}
|
||||||
onClick={handleDeleteAccount}
|
onClick={handleDeleteAccount}
|
||||||
>
|
>
|
||||||
@@ -209,97 +270,239 @@ const ProfileSettings: React.FC<{ onToast: (msg: string) => void; onError: (msg:
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const BillingSettings: React.FC<{ onToast: (msg: string) => void; onError: (msg: string) => void }> = ({ onError }) => {
|
const BillingSettings: React.FC<{
|
||||||
const navigate = useNavigate()
|
onToast: (msg: string) => void;
|
||||||
const [loading, setLoading] = useState(false)
|
onError: (msg: string) => void;
|
||||||
|
}> = ({ onError }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const { data: subscription } = useQuery({
|
const { data: subscription } = useQuery({
|
||||||
queryKey: ['subscription'],
|
queryKey: ["subscription"],
|
||||||
queryFn: billingAPI.getSubscription,
|
queryFn: billingAPI.getSubscription,
|
||||||
})
|
});
|
||||||
|
|
||||||
const handlePortal = async () => {
|
const handlePortal = async () => {
|
||||||
setLoading(true)
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const { url } = await billingAPI.createPortal(window.location.href)
|
const { url } = await billingAPI.createPortal(window.location.href);
|
||||||
window.location.href = url
|
window.location.href = url;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const e = err as { response?: { data?: { detail?: string } } }
|
const e = err as { response?: { data?: { detail?: string } } };
|
||||||
onError(e.response?.data?.detail || 'Failed to open billing portal')
|
onError(e.response?.data?.detail || "Failed to open billing portal");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const plan = subscription?.plan || 'free'
|
const plan = subscription?.plan || "free";
|
||||||
const isPaid = plan !== 'free'
|
const isPaid = plan !== "free";
|
||||||
|
|
||||||
const planFeatures: Record<string, { published: string; conversations: string; codeExport: string }> = {
|
const planFeatures: Record<
|
||||||
free: { published: '1', conversations: '100/month', codeExport: '❌ Agency+ only' },
|
string,
|
||||||
starter: { published: '1', conversations: '1,500/month', codeExport: '❌ Agency+ only' },
|
{ published: string; conversations: string; codeExport: string }
|
||||||
business: { published: '3', conversations: '5,000/month', codeExport: '❌ Agency+ only' },
|
> = {
|
||||||
agency: { published: 'Unlimited', conversations: '20,000/month', codeExport: '✅ Included' },
|
free: {
|
||||||
enterprise: { published: 'Unlimited', conversations: 'Unlimited', codeExport: '✅ Included' },
|
published: "1",
|
||||||
}
|
conversations: "100/month",
|
||||||
const features = planFeatures[plan] || planFeatures.free
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-6">
|
||||||
<Card className="p-6">
|
{/* Current Plan Card - Version with hover effect */}
|
||||||
<h2 className="font-semibold text-gray-900 mb-4">Current Plan</h2>
|
<Card className="group relative overflow-hidden border-0 shadow-lg transition-all duration-300 hover:shadow-2xl">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="absolute inset-0 bg-gradient-to-r from-blue-600/5 to-purple-600/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||||
<div>
|
<div className="p-6 relative">
|
||||||
<span className={`px-3 py-1 text-sm font-semibold rounded-full capitalize ${getPlanColor(plan)}`}>
|
<div className="flex items-start justify-between mb-6">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-gradient-to-r from-blue-500 to-purple-500 flex items-center justify-center">
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 text-white"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path d="M10 2a8 8 0 100 16 8 8 0 000-16zm1 14h-2v-2h2v2zm0-4h-2V7h2v5z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-bold text-gray-900">
|
||||||
|
Current Plan
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center gap-2 px-3 py-1.5 text-sm font-semibold rounded-full capitalize shadow-sm ${getPlanColor(plan)}`}
|
||||||
|
>
|
||||||
|
<span className="w-2 h-2 rounded-full bg-current"></span>
|
||||||
{plan}
|
{plan}
|
||||||
</span>
|
</span>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<div className="flex items-center gap-2">
|
||||||
Status:{' '}
|
<span className="text-xs text-gray-500">Status:</span>
|
||||||
<span className={subscription?.status === 'active' ? 'text-green-600' : 'text-red-600'}>
|
<span
|
||||||
{subscription?.status || 'active'}
|
className={`inline-flex items-center gap-1.5 text-xs font-medium px-2 py-1 rounded-full ${
|
||||||
|
subscription?.status === "active"
|
||||||
|
? "bg-green-100 text-green-700"
|
||||||
|
: "bg-red-100 text-red-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`w-1.5 h-1.5 rounded-full ${subscription?.status === "active" ? "bg-green-500" : "bg-red-500"}`}
|
||||||
|
></span>
|
||||||
|
{subscription?.status === "active"
|
||||||
|
? "Active"
|
||||||
|
: subscription?.status || "Active"}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{isPaid && subscription?.current_period_end && (
|
{isPaid && subscription?.current_period_end && (
|
||||||
<div className="text-right">
|
<div className="text-right bg-gray-50 rounded-lg px-4 py-2">
|
||||||
<p className="text-xs text-gray-500">Renews on</p>
|
<p className="text-xs text-gray-500 uppercase tracking-wide">
|
||||||
<p className="text-sm font-medium">{formatDate(subscription.current_period_end)}</p>
|
Renewal Date
|
||||||
|
</p>
|
||||||
|
<p className="text-sm font-bold text-gray-900">
|
||||||
|
{formatDate(subscription.current_period_end)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3 mt-6">
|
||||||
{!isPaid ? (
|
{!isPaid ? (
|
||||||
<Button onClick={() => navigate('/pricing')} className="flex-1">
|
<Button
|
||||||
Upgrade Plan
|
onClick={() => navigate("/pricing")}
|
||||||
|
className="w-full bg-gradient-to-r from-primary-600 to-indigo-600 hover:from-primary-700 hover:to-indigo-700 text-white shadow-md hover:shadow-lg transition-all duration-300 rounded-lg py-5 text-base font-semibold group"
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
✨ Upgrade Plan
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 transform group-hover:translate-x-1 transition-transform"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M9 5l7 7-7 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button variant="outline" onClick={handlePortal} loading={loading} className="flex-1">
|
<Button
|
||||||
<ExternalLink className="w-3.5 h-3.5" />
|
variant="outline"
|
||||||
|
onClick={handlePortal}
|
||||||
|
loading={loading}
|
||||||
|
className="flex-1 border-gray-300 hover:border-gray-400 hover:bg-gray-50 rounded-lg py-5 text-base font-semibold transition-all duration-300"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-4 h-4 mr-2" />
|
||||||
Manage Billing
|
Manage Billing
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Plan features */}
|
{/* Plan Features Card - Enhanced Version */}
|
||||||
<Card className="p-6">
|
<Card className="border-0 shadow-lg overflow-hidden">
|
||||||
<h3 className="font-semibold text-gray-900 mb-4">Plan Features</h3>
|
<div className="bg-gradient-to-r from-gray-50 to-white p-6">
|
||||||
<div className="space-y-3">
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center shadow-md">
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5 text-white"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-bold text-gray-900">Plan Features</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
{[
|
{[
|
||||||
{ label: 'Chatbots published', value: features.published },
|
{
|
||||||
{ label: 'Conversations/month', value: features.conversations },
|
label: "Chatbots published",
|
||||||
{ label: 'Code export', value: features.codeExport },
|
value: features.published,
|
||||||
].map(({ label, value }) => (
|
suffix: "chatbot(s)",
|
||||||
<div key={label} className="flex justify-between py-2 border-b border-gray-100 last:border-0">
|
},
|
||||||
<span className="text-sm text-gray-600">{label}</span>
|
{
|
||||||
<span className="text-sm font-medium text-gray-900">{value}</span>
|
label: "Conversations / month",
|
||||||
|
value: features.conversations,
|
||||||
|
suffix: "conversations",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Code export",
|
||||||
|
value: features.codeExport,
|
||||||
|
highlight: features.codeExport,
|
||||||
|
},
|
||||||
|
].map(({ label, value, suffix, highlight }) => (
|
||||||
|
<div
|
||||||
|
key={label}
|
||||||
|
className="flex items-center justify-between py-3 px-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200 group"
|
||||||
|
>
|
||||||
|
<span className="text-sm text-gray-600 group-hover:text-gray-800 transition-colors">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`text-sm font-semibold ${highlight === false ? "text-gray-400" : "text-gray-900"}`}
|
||||||
|
>
|
||||||
|
{value}{" "}
|
||||||
|
{suffix && (
|
||||||
|
<span className="text-xs text-gray-400 font-normal">
|
||||||
|
{suffix}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 pt-4 border-t border-gray-200 text-center">
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
{isPaid
|
||||||
|
? "💳 Simplified subscription management"
|
||||||
|
: "🚀 Unlock more features by upgrading your plan"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
allowedHosts: ['gripple-delena-triserial.ngrok-free.dev', '127.0.0.1', 'localhost'],
|
allowedHosts: ['gripple-delena-triserial.ngrok-free.dev', '127.0.0.1', 'localhost', 'contexta-production-672d.up.railway.app'],
|
||||||
host: true
|
host: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user