diff --git a/bun.lockb b/bun.lockb
index 4d5c249..7d464b6 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package-lock.json b/package-lock.json
index 0b0ca41..64b0398 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"dependencies": {
"@cloudflare/vite-plugin": "^1.25.5",
"@hookform/resolvers": "^5.2.2",
+ "@lovable.dev/cloud-auth-js": "^1.1.1",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.8",
@@ -34,8 +35,9 @@
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
+ "@supabase/supabase-js": "^2.103.3",
"@tailwindcss/vite": "^4.2.1",
- "@tanstack/react-query": "^5.83.0",
+ "@tanstack/react-query": "^5.99.0",
"@tanstack/react-router": "^1.168.0",
"@tanstack/react-start": "^1.167.14",
"@tanstack/router-plugin": "^1.167.10",
@@ -44,12 +46,14 @@
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
+ "framer-motion": "^12.38.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.575.0",
"react": "^19.2.0",
"react-day-picker": "^9.14.0",
"react-dom": "^19.2.0",
- "react-hook-form": "^7.71.2",
+ "react-hook-form": "^7.72.1",
+ "react-imask": "^7.6.1",
"react-resizable-panels": "^4.6.5",
"recharts": "^2.15.4",
"sonner": "^2.0.7",
@@ -58,11 +62,11 @@
"tw-animate-css": "^1.3.4",
"vaul": "^1.1.2",
"vite-tsconfig-paths": "^6.0.2",
- "zod": "^3.24.2"
+ "zod": "^4.3.6"
},
"devDependencies": {
"@eslint/js": "^9.32.0",
- "@lovable.dev/vite-tanstack-config": "^1.2.0",
+ "@lovable.dev/vite-tanstack-config": "^1.4.0",
"@types/node": "^22.16.5",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
@@ -120,7 +124,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -352,6 +355,18 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/runtime-corejs3": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.2.tgz",
+ "integrity": "sha512-Lc94FOD5+0aXhdb0Tdg3RUtqT6yWbI/BbFWvlaSJ3gAb9Ks+99nHRDKADVqC37er4eCB0fHyWT+y+K3QOvJKbw==",
+ "license": "MIT",
+ "dependencies": {
+ "core-js-pure": "^3.48.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/template": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
@@ -426,7 +441,6 @@
"resolved": "https://registry.npmjs.org/@cloudflare/vite-plugin/-/vite-plugin-1.32.2.tgz",
"integrity": "sha512-sEI/jusfDvzHIL4oiBBV5iUxAXfTRvguecIDWQ/AxBgEjGO1ZslHOEy4rlxfgqrdRWtq0TM9m3oC+hT32hajEg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@cloudflare/unenv-preset": "2.16.0",
"miniflare": "4.20260410.0",
@@ -1768,13 +1782,35 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@lovable.dev/cloud-auth-js": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@lovable.dev/cloud-auth-js/-/cloud-auth-js-1.1.1.tgz",
+ "integrity": "sha512-80elU8dSJG6bho0Xnfj2oy53wp883nYXrG1Wy948LC/ZUaUQ0i9EGXQFmwTLOBFrWqxb6aNaOlZUvQ8BVGhjMQ==",
+ "license": "MIT"
+ },
+ "node_modules/@lovable.dev/vite-plugin-hmr-gate": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@lovable.dev/vite-plugin-hmr-gate/-/vite-plugin-hmr-gate-1.0.0.tgz",
+ "integrity": "sha512-gWpiqRKwyE68ul7xlryGmoMJC3jJEWg/Pl80/Dl8h1Ajfw6VB3Quq8Mq17D5bAPL6NjxKlmA1g8GWJEjK2fDYg==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "vite": ">=5.0.0 <8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "vite": {
+ "optional": false
+ }
+ }
+ },
"node_modules/@lovable.dev/vite-tanstack-config": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@lovable.dev/vite-tanstack-config/-/vite-tanstack-config-1.2.0.tgz",
- "integrity": "sha512-vvCb3I9mCnRwg09Re1Fr7liG23kHy2luaTGkGllS1tWQOhhlTUYj8jGRCkRd7MDjxg6TxpEQdOVgiK98Rm7sMQ==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@lovable.dev/vite-tanstack-config/-/vite-tanstack-config-1.4.0.tgz",
+ "integrity": "sha512-OZmCj7t77kcKHEWANz7oeILdB1Y339h7cKz34SpNMxiyna+kzf8R57DZfBzMXY71A1UcOtCJYvSmr/wfDom8ag==",
"dev": true,
"license": "MIT",
"dependencies": {
+ "@lovable.dev/vite-plugin-hmr-gate": "^1.0.0",
"lovable-tagger": "1.1.13"
},
"peerDependencies": {
@@ -3550,6 +3586,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3563,6 +3600,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3576,6 +3614,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3589,6 +3628,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3602,6 +3642,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3615,6 +3656,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3628,6 +3670,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3641,6 +3684,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3654,6 +3698,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3667,6 +3712,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3680,6 +3726,7 @@
"cpu": [
"loong64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3693,6 +3740,7 @@
"cpu": [
"loong64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3706,6 +3754,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3719,6 +3768,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3732,6 +3782,7 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3745,6 +3796,7 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3758,6 +3810,7 @@
"cpu": [
"s390x"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3771,6 +3824,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3784,6 +3838,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3797,6 +3852,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3810,6 +3866,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3823,6 +3880,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3836,6 +3894,7 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3849,6 +3908,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3862,6 +3922,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3892,6 +3953,113 @@
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
+ "node_modules/@supabase/auth-js": {
+ "version": "2.103.3",
+ "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.103.3.tgz",
+ "integrity": "sha512-SMDJ4vg5jLXNEHdhN4J4ujSb203WangbDw1n3VaARH0ZqM51E6lJnoUAHlpQU9N7SzP0hfgghA9IvT8c7tGRfg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/functions-js": {
+ "version": "2.103.3",
+ "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.103.3.tgz",
+ "integrity": "sha512-A2ZHi95GIRRlN9LGOSa/zGEIPg9taR1giDI9Gkfkgrcz0YmKV8ShiAplIrKsHQFdkzKxtsO3maJF0efL+i31mg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/phoenix": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.0.tgz",
+ "integrity": "sha512-RHSx8bHS02xwfHdAbX5Lpbo6PXbgyf7lTaXTlwtFDPwOIw64NnVRwFAXGojHhjtVYI+PEPNSWwkL90f4agN3bw==",
+ "license": "MIT"
+ },
+ "node_modules/@supabase/postgrest-js": {
+ "version": "2.103.3",
+ "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.103.3.tgz",
+ "integrity": "sha512-S0k/9FJVXDeejNfQLCJwRlm4IH8Wet/HEEdBTBpX6/G2o1eU/6CjQop/hJPZIwlQkI6D/zbHH8KymuCsBgy6jA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/realtime-js": {
+ "version": "2.103.3",
+ "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.103.3.tgz",
+ "integrity": "sha512-fUvKtSXMUk1BkApVwAurWtHF4Vzbb0UB9aC/fQXrRBek7Ta3Kaora+wHf/fGwFNQs7uRz+mvjIVpzLfpR32VXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/phoenix": "^0.4.0",
+ "@types/ws": "^8.18.1",
+ "tslib": "2.8.1",
+ "ws": "^8.18.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/realtime-js/node_modules/ws": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
+ "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@supabase/storage-js": {
+ "version": "2.103.3",
+ "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.103.3.tgz",
+ "integrity": "sha512-5bAIEubrw5keHcdKR2RTois0O1M2Ilx4UYuzOzc07G6mLGCPS/8t1nbC6Vq451pnxR3sK+rmtFHWb9CY/OPjAw==",
+ "license": "MIT",
+ "dependencies": {
+ "iceberg-js": "^0.8.1",
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/supabase-js": {
+ "version": "2.103.3",
+ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.103.3.tgz",
+ "integrity": "sha512-DuPiAz5pIJsTAQCt7B6bDZrnLzlq9+/5bta/GWTsgpLn6AkuZQcmYsQHYplv4skQ8U2raKY5HASQOu4KtYq9Qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/auth-js": "2.103.3",
+ "@supabase/functions-js": "2.103.3",
+ "@supabase/postgrest-js": "2.103.3",
+ "@supabase/realtime-js": "2.103.3",
+ "@supabase/storage-js": "2.103.3"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@tabby_ai/hijri-converter": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@tabby_ai/hijri-converter/-/hijri-converter-1.0.5.tgz",
@@ -4112,6 +4280,64 @@
"node": ">=14.0.0"
}
},
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
+ "version": "1.8.1",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.1.0",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
+ "version": "1.8.1",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
+ "version": "1.1.0",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.1",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1",
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
+ "version": "2.8.1",
+ "inBundle": true,
+ "license": "0BSD",
+ "optional": true
+ },
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz",
@@ -4149,7 +4375,6 @@
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz",
"integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@tailwindcss/node": "4.2.2",
"@tailwindcss/oxide": "4.2.2",
@@ -4226,7 +4451,6 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-start/-/react-start-1.167.39.tgz",
"integrity": "sha512-57vpXGWhrisb69mgYBlEmpNvZCKYKix4jj4vqkiInCM4b97Y3SA8fwTPRXs7L/kEXGZCkCFzMR6EDiP15a3rWg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@tanstack/react-router": "1.168.21",
"@tanstack/react-start-client": "1.166.38",
@@ -4403,6 +4627,15 @@
"url": "https://github.com/sponsors/tannerlinsley"
}
},
+ "node_modules/@tanstack/router-generator/node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
"node_modules/@tanstack/router-plugin": {
"version": "1.167.22",
"resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.167.22.tgz",
@@ -4458,6 +4691,15 @@
}
}
},
+ "node_modules/@tanstack/router-plugin/node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
"node_modules/@tanstack/router-utils": {
"version": "1.161.6",
"resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.161.6.tgz",
@@ -4571,6 +4813,15 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@tanstack/start-plugin-core/node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
"node_modules/@tanstack/start-server-core": {
"version": "1.167.19",
"resolved": "https://registry.npmjs.org/@tanstack/start-server-core/-/start-server-core-1.167.19.tgz",
@@ -4749,6 +5000,7 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
@@ -4762,9 +5014,7 @@
"version": "22.19.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
"integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==",
- "devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -4773,9 +5023,8 @@
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -4784,13 +5033,21 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
- "peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
},
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.58.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz",
@@ -4836,7 +5093,6 @@
"integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.58.2",
"@typescript-eslint/types": "8.58.2",
@@ -5120,7 +5376,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5334,7 +5589,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
@@ -5571,6 +5825,17 @@
"integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==",
"license": "MIT"
},
+ "node_modules/core-js-pure": {
+ "version": "3.49.0",
+ "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.49.0.tgz",
+ "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -5913,8 +6178,7 @@
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/embla-carousel-react": {
"version": "8.6.0",
@@ -6065,7 +6329,6 @@
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -6126,7 +6389,6 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -6454,6 +6716,33 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/framer-motion": {
+ "version": "12.38.0",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
+ "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.38.0",
+ "motion-utils": "^12.36.0",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -6624,6 +6913,15 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/iceberg-js": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
+ "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -6646,6 +6944,18 @@
"node": ">= 4"
}
},
+ "node_modules/imask": {
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/imask/-/imask-7.6.1.tgz",
+ "integrity": "sha512-sJlIFM7eathUEMChTh9Mrfw/IgiWgJqBKq2VNbyXvBZ7ev/IlO6/KQTKlV/Fm+viQMLrFLG/zCuudrLIwgK2dg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.24.4"
+ },
+ "engines": {
+ "npm": ">=4.0.0"
+ }
+ },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -7362,6 +7672,21 @@
"node": "*"
}
},
+ "node_modules/motion-dom": {
+ "version": "12.38.0",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
+ "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.36.0"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.36.0",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
+ "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -7384,6 +7709,7 @@
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
"funding": [
{
"type": "github",
@@ -7644,6 +7970,7 @@
"version": "8.5.9",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
"integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
+ "dev": true,
"funding": [
{
"type": "opencollective",
@@ -7659,7 +7986,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -7818,7 +8144,6 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.2.tgz",
"integrity": "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==",
"license": "MIT",
- "peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -7895,7 +8220,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -7927,7 +8251,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
"integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -7940,7 +8263,6 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.72.1.tgz",
"integrity": "sha512-RhwBoy2ygeVZje+C+bwJ8g0NjTdBmDlJvAUHTxRjTmSUKPYsKfMphkS2sgEMotsY03bP358yEYlnUeZy//D9Ig==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -7952,6 +8274,22 @@
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
+ "node_modules/react-imask": {
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/react-imask/-/react-imask-7.6.1.tgz",
+ "integrity": "sha512-vLNfzcCz62Yzx/GRGh5tiCph9Gbh2cZu+Tz8OiO5it2eNuuhpA0DWhhSlOtVtSJ80+Bx+vFK5De8eQ9AmbkXzA==",
+ "license": "MIT",
+ "dependencies": {
+ "imask": "^7.6.1",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "npm": ">=4.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=0.14.0"
+ }
+ },
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -8200,6 +8538,7 @@
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
@@ -8296,7 +8635,6 @@
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.2.tgz",
"integrity": "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10"
}
@@ -9144,9 +9482,8 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -9198,7 +9535,6 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/unenv": {
@@ -9206,7 +9542,6 @@
"resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",
"integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"pathe": "^2.0.3"
}
@@ -9364,8 +9699,8 @@
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
+ "dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -9440,7 +9775,6 @@
"resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-6.1.1.tgz",
"integrity": "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"debug": "^4.1.1",
"globrex": "^0.1.2",
@@ -9457,6 +9791,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9473,6 +9808,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9489,6 +9825,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9505,6 +9842,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9521,6 +9859,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9537,6 +9876,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9553,6 +9893,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9569,6 +9910,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9585,6 +9927,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9601,6 +9944,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9617,6 +9961,7 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9633,6 +9978,7 @@
"cpu": [
"loong64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9649,6 +9995,7 @@
"cpu": [
"mips64el"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9665,6 +10012,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9681,6 +10029,7 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9697,6 +10046,7 @@
"cpu": [
"s390x"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9713,6 +10063,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9729,6 +10080,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9745,6 +10097,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9761,6 +10114,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9777,6 +10131,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9793,6 +10148,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9809,6 +10165,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9825,6 +10182,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9841,6 +10199,7 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9857,6 +10216,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9870,6 +10230,7 @@
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
+ "dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -9986,7 +10347,6 @@
"integrity": "sha512-T/GRD6Y5vN9g4CnGmOlfST1w7bj+1IjRFvX0K7CodZPJuPVPNPGhz8Wppah0WdT6A7I8Kad3zgZ2OkDdWtENrg==",
"hasInstallScript": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"workerd": "bin/workerd"
},
@@ -10571,9 +10931,9 @@
}
},
"node_modules/zod": {
- "version": "3.25.76",
- "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
- "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
diff --git a/package.json b/package.json
index c5445e7..2bd89c9 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"dependencies": {
"@cloudflare/vite-plugin": "^1.25.5",
"@hookform/resolvers": "^5.2.2",
+ "@lovable.dev/cloud-auth-js": "^1.1.1",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.8",
diff --git a/src/components/mika/AuthCard.tsx b/src/components/mika/AuthCard.tsx
new file mode 100644
index 0000000..12fcb0b
--- /dev/null
+++ b/src/components/mika/AuthCard.tsx
@@ -0,0 +1,38 @@
+import { Link } from "@tanstack/react-router";
+import { Logo } from "./Logo";
+import { ThemeToggle } from "./ThemeToggle";
+
+export function AuthCard({
+ title,
+ subtitle,
+ children,
+ footer,
+}: {
+ title: string;
+ subtitle?: string;
+ children: React.ReactNode;
+ footer?: React.ReactNode;
+}) {
+ return (
+
+
+
+
+
+
+
{title}
+ {subtitle &&
{subtitle}
}
+
+ {children}
+
+ {footer &&
{footer}
}
+
+
+
+ );
+}
diff --git a/src/components/mika/EnterpriseLeadForm.tsx b/src/components/mika/EnterpriseLeadForm.tsx
index dd86d4f..3b06129 100644
--- a/src/components/mika/EnterpriseLeadForm.tsx
+++ b/src/components/mika/EnterpriseLeadForm.tsx
@@ -6,6 +6,8 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { IMaskInput } from "react-imask";
import { toast } from "sonner";
+import { supabase } from "@/integrations/supabase/client";
+import { translateAuthError } from "@/lib/auth-errors";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
@@ -37,17 +39,22 @@ export function EnterpriseLeadForm() {
const onSubmit = async (data: FormValues) => {
setSubmitting(true);
- try {
- // Etapa 2: gravar em enterprise_leads via supabase.from(...).insert(...)
- // Por ora: feedback otimista para o usuário.
- await new Promise((r) => setTimeout(r, 700));
- toast.success("Recebemos seu contato! Falamos com você em até 1 dia útil.");
- reset();
- } catch {
- toast.error("Algo deu errado do nosso lado. Já estamos investigando.");
- } finally {
- setSubmitting(false);
+ const { error } = await supabase.from("enterprise_leads").insert({
+ company_name: data.company_name,
+ contact_name: data.contact_name,
+ email: data.email,
+ phone: data.phone,
+ team_size: data.team_size,
+ message: data.message || null,
+ status: "new",
+ });
+ setSubmitting(false);
+ if (error) {
+ toast.error(translateAuthError(error.message));
+ return;
}
+ toast.success("Recebemos seu contato! Falamos com você em até 1 dia útil.");
+ reset();
};
const phone = watch("phone") || "";
diff --git a/src/components/mika/GoogleButton.tsx b/src/components/mika/GoogleButton.tsx
new file mode 100644
index 0000000..59c87b7
--- /dev/null
+++ b/src/components/mika/GoogleButton.tsx
@@ -0,0 +1,55 @@
+"use client";
+
+import { useState } from "react";
+import { toast } from "sonner";
+import { Loader2 } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { lovable } from "@/integrations/lovable/index";
+import { translateAuthError } from "@/lib/auth-errors";
+
+export function GoogleButton({ redirectTo }: { redirectTo?: string }) {
+ const [loading, setLoading] = useState(false);
+
+ const onClick = async () => {
+ setLoading(true);
+ try {
+ const url = redirectTo
+ ? `${window.location.origin}${redirectTo}`
+ : window.location.origin;
+ const result = await lovable.auth.signInWithOAuth("google", { redirect_uri: url });
+ if (result.error) {
+ toast.error(translateAuthError(result.error.message));
+ setLoading(false);
+ return;
+ }
+ if (result.redirected) return;
+ // tokens recebidos: redireciona
+ window.location.href = url;
+ } catch (e) {
+ toast.error(translateAuthError(e instanceof Error ? e.message : String(e)));
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/components/mika/LandingHeader.tsx b/src/components/mika/LandingHeader.tsx
index 0c65c82..dc7cf5c 100644
--- a/src/components/mika/LandingHeader.tsx
+++ b/src/components/mika/LandingHeader.tsx
@@ -5,6 +5,7 @@ import { Link } from "@tanstack/react-router";
import { Menu } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Sheet, SheetContent, SheetTrigger, SheetTitle, SheetHeader } from "@/components/ui/sheet";
+import { useAuth } from "@/hooks/use-auth";
import { Logo } from "./Logo";
import { ThemeToggle } from "./ThemeToggle";
import { cn } from "@/lib/utils";
@@ -19,6 +20,7 @@ const NAV = [
export function LandingHeader() {
const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false);
+ const { user } = useAuth();
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 8);
@@ -53,12 +55,20 @@ export function LandingHeader() {
-
-
+ {user ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
@@ -86,12 +96,20 @@ export function LandingHeader() {
))}
-
-
+ {user ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
diff --git a/src/components/mika/PasswordStrengthMeter.tsx b/src/components/mika/PasswordStrengthMeter.tsx
new file mode 100644
index 0000000..1c5fe85
--- /dev/null
+++ b/src/components/mika/PasswordStrengthMeter.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import { passwordStrength, passwordStrengthLabel } from "@/lib/password";
+import { cn } from "@/lib/utils";
+
+export function PasswordStrengthMeter({ password }: { password: string }) {
+ if (!password) return null;
+ const score = passwordStrength(password);
+ const colors = [
+ "bg-destructive",
+ "bg-destructive",
+ "bg-warning",
+ "bg-success",
+ ];
+ return (
+
+
+ {[0, 1, 2].map((i) => (
+
+ ))}
+
+
+ Força: {passwordStrengthLabel(score)}
+
+
+ );
+}
diff --git a/src/components/mika/PlansSection.tsx b/src/components/mika/PlansSection.tsx
index f06b2be..cedaa4c 100644
--- a/src/components/mika/PlansSection.tsx
+++ b/src/components/mika/PlansSection.tsx
@@ -3,91 +3,63 @@
import { useState } from "react";
import { Link } from "@tanstack/react-router";
import { Check } from "lucide-react";
+import { useQuery } from "@tanstack/react-query";
+import { supabase } from "@/integrations/supabase/client";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Badge } from "@/components/ui/badge";
-import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from "@/components/ui/dialog";
+import { Skeleton } from "@/components/ui/skeleton";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogTrigger,
+} from "@/components/ui/dialog";
import { EnterpriseLeadForm } from "./EnterpriseLeadForm";
import { cn } from "@/lib/utils";
-// Fallback estático — na Etapa 2 será substituído por query Supabase à tabela `plans`.
-const STATIC_PLANS = [
- {
- slug: "basic",
- name: "Basic",
- description: "Para começar a usar IA no dia a dia.",
- monthly: 69.9,
- yearly: 671.04,
- highlighted: false,
- is_enterprise: false,
- features: [
- "1 agente pessoal no Telegram",
- "Memória persistente básica",
- "Integração Google Workspace",
- "5 skills personalizadas",
- "Suporte por e-mail",
- ],
- },
- {
- slug: "starter",
- name: "Starter",
- description: "Para quem usa IA todos os dias.",
- monthly: 199.9,
- yearly: 1919.04,
- highlighted: false,
- is_enterprise: false,
- features: [
- "Tudo do Basic",
- "Memória avançada e contextual",
- "Skills ilimitadas",
- "Agendamentos automáticos",
- "Suporte prioritário",
- ],
- },
- {
- slug: "professional",
- name: "Professional",
- description: "Para profissionais e times pequenos.",
- monthly: 399.9,
- yearly: 3839.04,
- highlighted: true,
- is_enterprise: false,
- features: [
- "Tudo do Starter",
- "VPS dedicada de alta performance",
- "Modelos de IA premium",
- "Integrações personalizadas",
- "Onboarding 1:1",
- "Suporte em horário estendido",
- ],
- },
- {
- slug: "enterprise",
- name: "Enterprise",
- description: "Para empresas com necessidades específicas.",
- monthly: null,
- yearly: null,
- highlighted: false,
- is_enterprise: true,
- features: [
- "Tudo do Professional",
- "Múltiplos agentes",
- "SSO e gestão de equipe",
- "SLA contratual",
- "Treinamento da equipe",
- "Account manager dedicado",
- ],
- },
-] as const;
+interface PlanRow {
+ id: string;
+ slug: string;
+ name: string;
+ description: string | null;
+ price_monthly_brl: number | null;
+ price_yearly_brl: number | null;
+ features: string[];
+ highlighted: boolean;
+ is_enterprise: boolean;
+ display_order: number;
+}
const fmtBRL = (v: number) =>
v.toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+function usePlans() {
+ return useQuery({
+ queryKey: ["plans"],
+ staleTime: 5 * 60_000,
+ queryFn: async (): Promise
=> {
+ const { data, error } = await supabase
+ .from("plans")
+ .select("*")
+ .order("display_order", { ascending: true });
+ if (error) throw error;
+ return (data || []).map((p) => ({
+ ...p,
+ features: Array.isArray(p.features) ? (p.features as string[]) : [],
+ })) as PlanRow[];
+ },
+ });
+}
+
export function PlansSection() {
const [yearly, setYearly] = useState(false);
+ const { data: plans, isLoading } = usePlans();
return (
-
+
Planos para cada estágio
@@ -96,110 +68,140 @@ export function PlansSection() {
-
+
Mensal
-
-
+
+
Anual
- 20% off
+
+ 20% off
+
- {STATIC_PLANS.map((plan) => {
- const price = yearly ? plan.yearly : plan.monthly;
- const monthlyEquivalent = yearly && plan.yearly ? plan.yearly / 12 : null;
- return (
-
- {plan.highlighted && (
-
- Mais popular
-
- )}
+ {isLoading
+ ? Array.from({ length: 4 }).map((_, i) => (
+
+ ))
+ : (plans || []).map((plan) => {
+ const monthly = plan.price_monthly_brl ?? 0;
+ const yearlyPrice = plan.price_yearly_brl ?? 0;
+ const monthlyEquivalent = yearly && yearlyPrice ? yearlyPrice / 12 : monthly;
+ return (
+
+ {plan.highlighted && (
+
+ Mais popular
+
+ )}
-
-
{plan.name}
-
{plan.description}
-
+
+
{plan.name}
+
{plan.description}
+
-
- {plan.is_enterprise ? (
-
Sob consulta
- ) : (
- <>
-
- R$
-
- {fmtBRL(yearly && monthlyEquivalent ? monthlyEquivalent : (price as number))}
-
- /mês
-
- {yearly && (
-
- R$ {fmtBRL(plan.yearly!)} cobrados anualmente
-
+
+ {plan.is_enterprise ? (
+
Sob consulta
+ ) : (
+ <>
+
+ R$
+
+ {fmtBRL(monthlyEquivalent)}
+
+ /mês
+
+ {yearly && yearlyPrice > 0 && (
+
+ R$ {fmtBRL(yearlyPrice)} cobrados anualmente
+
+ )}
+ >
)}
- >
- )}
-
+
-
- {plan.features.map((f) => (
- -
-
- {f}
-
- ))}
-
+
+ {plan.features.map((f) => (
+ -
+
+ {f}
+
+ ))}
+
-
- {plan.is_enterprise ? (
-
- ) : (
-
- )}
-
- Sujeito à política de uso justo. Veja termos.
-
-
-
- );
- })}
+
+ Sujeito à política de uso justo. Veja termos.
+
+
+
+ );
+ })}
diff --git a/src/components/mika/SubscriptionBanner.tsx b/src/components/mika/SubscriptionBanner.tsx
new file mode 100644
index 0000000..f45b318
--- /dev/null
+++ b/src/components/mika/SubscriptionBanner.tsx
@@ -0,0 +1,112 @@
+"use client";
+
+import { format } from "date-fns";
+import { ptBR } from "date-fns/locale";
+import { AlertCircle, AlertTriangle, Info } from "lucide-react";
+import { Link } from "@tanstack/react-router";
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import type { SubscriptionRow } from "@/hooks/use-profile";
+
+export function SubscriptionBanner({ subscription }: { subscription: SubscriptionRow | null }) {
+ if (!subscription) return null;
+
+ const periodEnd = subscription.current_period_end ? new Date(subscription.current_period_end) : null;
+ const periodEndPassed = periodEnd && periodEnd.getTime() < Date.now();
+
+ // Pagamento pendente
+ if (subscription.status === "past_due") {
+ return (
+
+ Atualizar pagamento
+
+ }
+ />
+ );
+ }
+
+ // Cancelamento agendado
+ if (subscription.cancel_at_period_end && periodEnd && !periodEndPassed) {
+ return (
+
+ Reativar assinatura
+
+ }
+ />
+ );
+ }
+
+ // Cancelada e período já passou
+ if (subscription.status === "canceled" && periodEndPassed) {
+ return (
+
+ Assinar novamente
+
+ }
+ />
+ );
+ }
+
+ return null;
+}
+
+function Banner({
+ tone,
+ icon: Icon,
+ title,
+ description,
+ action,
+}: {
+ tone: "destructive" | "warning" | "muted";
+ icon: React.ComponentType<{ className?: string }>;
+ title: string;
+ description: string;
+ action: React.ReactNode;
+}) {
+ const tones = {
+ destructive: "bg-destructive/10 border-destructive/30 text-destructive",
+ warning: "bg-warning/10 border-warning/30 text-warning",
+ muted: "bg-muted border-border text-foreground",
+ };
+ return (
+
+
+
+
+
{title}
+
+ {description}
+
+
+
+
{action}
+
+ );
+}
diff --git a/src/hooks/use-auth.ts b/src/hooks/use-auth.ts
new file mode 100644
index 0000000..6522992
--- /dev/null
+++ b/src/hooks/use-auth.ts
@@ -0,0 +1,34 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import type { Session, User } from "@supabase/supabase-js";
+import { supabase } from "@/integrations/supabase/client";
+
+interface AuthState {
+ user: User | null;
+ session: Session | null;
+ loading: boolean;
+}
+
+export function useAuth(): AuthState {
+ const [state, setState] = useState({
+ user: null,
+ session: null,
+ loading: true,
+ });
+
+ useEffect(() => {
+ // CRITICAL: subscribe BEFORE getSession (avoid missed events)
+ const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
+ setState({ user: session?.user ?? null, session, loading: false });
+ });
+
+ supabase.auth.getSession().then(({ data: { session } }) => {
+ setState({ user: session?.user ?? null, session, loading: false });
+ });
+
+ return () => subscription.unsubscribe();
+ }, []);
+
+ return state;
+}
diff --git a/src/hooks/use-profile.ts b/src/hooks/use-profile.ts
new file mode 100644
index 0000000..7855eec
--- /dev/null
+++ b/src/hooks/use-profile.ts
@@ -0,0 +1,68 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import { supabase } from "@/integrations/supabase/client";
+import { useAuth } from "./use-auth";
+
+export interface Profile {
+ id: string;
+ full_name: string;
+ company_name: string | null;
+ cpf_cnpj: string | null;
+ phone: string | null;
+ avatar_url: string | null;
+ stripe_customer_id: string | null;
+ onboarding_completed: boolean;
+}
+
+export function useProfile() {
+ const { user } = useAuth();
+
+ return useQuery({
+ queryKey: ["profile", user?.id],
+ enabled: !!user,
+ queryFn: async (): Promise => {
+ if (!user) return null;
+ const { data, error } = await supabase
+ .from("profiles")
+ .select("*")
+ .eq("id", user.id)
+ .maybeSingle();
+ if (error) throw error;
+ return data as Profile | null;
+ },
+ });
+}
+
+export interface SubscriptionRow {
+ id: string;
+ user_id: string;
+ plan_id: string | null;
+ stripe_subscription_id: string | null;
+ status: "active" | "trialing" | "past_due" | "canceled" | "incomplete" | "incomplete_expired" | "unpaid";
+ billing_cycle: "monthly" | "yearly";
+ current_period_start: string | null;
+ current_period_end: string | null;
+ cancel_at_period_end: boolean;
+}
+
+export function useSubscription() {
+ const { user } = useAuth();
+
+ return useQuery({
+ queryKey: ["subscription", user?.id],
+ enabled: !!user,
+ queryFn: async (): Promise => {
+ if (!user) return null;
+ const { data, error } = await supabase
+ .from("subscriptions")
+ .select("*")
+ .eq("user_id", user.id)
+ .order("created_at", { ascending: false })
+ .limit(1)
+ .maybeSingle();
+ if (error) throw error;
+ return data as SubscriptionRow | null;
+ },
+ });
+}
diff --git a/src/integrations/lovable/index.ts b/src/integrations/lovable/index.ts
new file mode 100644
index 0000000..4561eb5
--- /dev/null
+++ b/src/integrations/lovable/index.ts
@@ -0,0 +1,38 @@
+// This file is auto-generated by Lovable. Do not modify it.
+
+import { createLovableAuth } from "@lovable.dev/cloud-auth-js";
+import { supabase } from "../supabase/client";
+const lovableAuth = createLovableAuth();
+
+type SignInOptions = {
+ redirect_uri?: string;
+ extraParams?: Record;
+};
+
+export const lovable = {
+ auth: {
+ signInWithOAuth: async (provider: "google" | "apple" | "microsoft", opts?: SignInOptions) => {
+ const result = await lovableAuth.signInWithOAuth(provider, {
+ redirect_uri: opts?.redirect_uri,
+ extraParams: {
+ ...opts?.extraParams,
+ },
+ });
+
+ if (result.redirected) {
+ return result;
+ }
+
+ if (result.error) {
+ return result;
+ }
+
+ try {
+ await supabase.auth.setSession(result.tokens);
+ } catch (e) {
+ return { error: e instanceof Error ? e : new Error(String(e)) };
+ }
+ return result;
+ },
+ },
+};
diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts
index ba7e5bb..c3e0690 100644
--- a/src/integrations/supabase/types.ts
+++ b/src/integrations/supabase/types.ts
@@ -14,7 +14,257 @@ export type Database = {
}
public: {
Tables: {
- [_ in never]: never
+ agent_instances: {
+ Row: {
+ container_name: string | null
+ created_at: string
+ id: string
+ status: string
+ telegram_bot_token_vault_id: string | null
+ telegram_bot_username: string | null
+ updated_at: string
+ user_id: string
+ uuid_tenant: string
+ vps_host: string | null
+ }
+ Insert: {
+ container_name?: string | null
+ created_at?: string
+ id?: string
+ status?: string
+ telegram_bot_token_vault_id?: string | null
+ telegram_bot_username?: string | null
+ updated_at?: string
+ user_id: string
+ uuid_tenant?: string
+ vps_host?: string | null
+ }
+ Update: {
+ container_name?: string | null
+ created_at?: string
+ id?: string
+ status?: string
+ telegram_bot_token_vault_id?: string | null
+ telegram_bot_username?: string | null
+ updated_at?: string
+ user_id?: string
+ uuid_tenant?: string
+ vps_host?: string | null
+ }
+ Relationships: [
+ {
+ foreignKeyName: "agent_instances_user_id_fkey"
+ columns: ["user_id"]
+ isOneToOne: true
+ referencedRelation: "profiles"
+ referencedColumns: ["id"]
+ },
+ ]
+ }
+ enterprise_leads: {
+ Row: {
+ company_name: string
+ contact_name: string
+ created_at: string
+ email: string
+ id: string
+ message: string | null
+ phone: string | null
+ status: string
+ team_size: string
+ }
+ Insert: {
+ company_name: string
+ contact_name: string
+ created_at?: string
+ email: string
+ id?: string
+ message?: string | null
+ phone?: string | null
+ status?: string
+ team_size: string
+ }
+ Update: {
+ company_name?: string
+ contact_name?: string
+ created_at?: string
+ email?: string
+ id?: string
+ message?: string | null
+ phone?: string | null
+ status?: string
+ team_size?: string
+ }
+ Relationships: []
+ }
+ plans: {
+ Row: {
+ created_at: string
+ description: string | null
+ display_order: number
+ features: Json
+ highlighted: boolean
+ id: string
+ is_enterprise: boolean
+ name: string
+ price_monthly_brl: number | null
+ price_yearly_brl: number | null
+ slug: string
+ stripe_price_id_monthly: string | null
+ stripe_price_id_yearly: string | null
+ }
+ Insert: {
+ created_at?: string
+ description?: string | null
+ display_order?: number
+ features?: Json
+ highlighted?: boolean
+ id?: string
+ is_enterprise?: boolean
+ name: string
+ price_monthly_brl?: number | null
+ price_yearly_brl?: number | null
+ slug: string
+ stripe_price_id_monthly?: string | null
+ stripe_price_id_yearly?: string | null
+ }
+ Update: {
+ created_at?: string
+ description?: string | null
+ display_order?: number
+ features?: Json
+ highlighted?: boolean
+ id?: string
+ is_enterprise?: boolean
+ name?: string
+ price_monthly_brl?: number | null
+ price_yearly_brl?: number | null
+ slug?: string
+ stripe_price_id_monthly?: string | null
+ stripe_price_id_yearly?: string | null
+ }
+ Relationships: []
+ }
+ profiles: {
+ Row: {
+ avatar_url: string | null
+ company_name: string | null
+ cpf_cnpj: string | null
+ created_at: string
+ full_name: string
+ id: string
+ onboarding_completed: boolean
+ phone: string | null
+ stripe_customer_id: string | null
+ updated_at: string
+ }
+ Insert: {
+ avatar_url?: string | null
+ company_name?: string | null
+ cpf_cnpj?: string | null
+ created_at?: string
+ full_name?: string
+ id: string
+ onboarding_completed?: boolean
+ phone?: string | null
+ stripe_customer_id?: string | null
+ updated_at?: string
+ }
+ Update: {
+ avatar_url?: string | null
+ company_name?: string | null
+ cpf_cnpj?: string | null
+ created_at?: string
+ full_name?: string
+ id?: string
+ onboarding_completed?: boolean
+ phone?: string | null
+ stripe_customer_id?: string | null
+ updated_at?: string
+ }
+ Relationships: []
+ }
+ stripe_webhook_events: {
+ Row: {
+ event_type: string
+ id: string
+ payload: Json | null
+ processed_at: string
+ stripe_event_id: string
+ }
+ Insert: {
+ event_type: string
+ id?: string
+ payload?: Json | null
+ processed_at?: string
+ stripe_event_id: string
+ }
+ Update: {
+ event_type?: string
+ id?: string
+ payload?: Json | null
+ processed_at?: string
+ stripe_event_id?: string
+ }
+ Relationships: []
+ }
+ subscriptions: {
+ Row: {
+ billing_cycle: string
+ cancel_at_period_end: boolean
+ created_at: string
+ current_period_end: string | null
+ current_period_start: string | null
+ id: string
+ plan_id: string | null
+ status: string
+ stripe_subscription_id: string | null
+ updated_at: string
+ user_id: string
+ }
+ Insert: {
+ billing_cycle: string
+ cancel_at_period_end?: boolean
+ created_at?: string
+ current_period_end?: string | null
+ current_period_start?: string | null
+ id?: string
+ plan_id?: string | null
+ status: string
+ stripe_subscription_id?: string | null
+ updated_at?: string
+ user_id: string
+ }
+ Update: {
+ billing_cycle?: string
+ cancel_at_period_end?: boolean
+ created_at?: string
+ current_period_end?: string | null
+ current_period_start?: string | null
+ id?: string
+ plan_id?: string | null
+ status?: string
+ stripe_subscription_id?: string | null
+ updated_at?: string
+ user_id?: string
+ }
+ Relationships: [
+ {
+ foreignKeyName: "subscriptions_plan_id_fkey"
+ columns: ["plan_id"]
+ isOneToOne: false
+ referencedRelation: "plans"
+ referencedColumns: ["id"]
+ },
+ {
+ foreignKeyName: "subscriptions_user_id_fkey"
+ columns: ["user_id"]
+ isOneToOne: false
+ referencedRelation: "profiles"
+ referencedColumns: ["id"]
+ },
+ ]
+ }
}
Views: {
[_ in never]: never
diff --git a/src/lib/auth-errors.ts b/src/lib/auth-errors.ts
new file mode 100644
index 0000000..27c8c0a
--- /dev/null
+++ b/src/lib/auth-errors.ts
@@ -0,0 +1,26 @@
+/**
+ * Traduz mensagens de erro do Supabase Auth para mensagens em português específicas.
+ */
+export function translateAuthError(message: string | undefined): string {
+ if (!message) return "Algo deu errado. Tente novamente.";
+ const m = message.toLowerCase();
+
+ if (m.includes("invalid login credentials") || m.includes("invalid_credentials"))
+ return "E-mail ou senha incorretos. Tente novamente.";
+ if (m.includes("user already registered") || m.includes("already been registered") || m.includes("already exists"))
+ return "Esse e-mail já possui cadastro. Deseja fazer login?";
+ if (m.includes("password") && (m.includes("short") || m.includes("weak") || m.includes("characters")))
+ return "A senha precisa de pelo menos 8 caracteres, 1 maiúscula e 1 número.";
+ if (m.includes("email") && m.includes("invalid"))
+ return "Formato de e-mail inválido.";
+ if (m.includes("rate limit") || m.includes("too many requests"))
+ return "Muitas tentativas. Aguarde 1 minuto e tente novamente.";
+ if (m.includes("expired") || m.includes("jwt") || m.includes("session"))
+ return "Sua sessão expirou. Faça login novamente.";
+ if (m.includes("email not confirmed"))
+ return "Confirme seu e-mail antes de entrar. Verifique sua caixa de entrada.";
+ if (m.includes("pwned") || m.includes("compromised") || m.includes("breach"))
+ return "Esta senha apareceu em vazamentos públicos. Escolha outra mais forte.";
+
+ return message;
+}
diff --git a/src/lib/password.ts b/src/lib/password.ts
new file mode 100644
index 0000000..58ffa5f
--- /dev/null
+++ b/src/lib/password.ts
@@ -0,0 +1,14 @@
+export type PasswordStrength = 0 | 1 | 2 | 3;
+
+export function passwordStrength(password: string): PasswordStrength {
+ let score = 0;
+ if (password.length >= 8) score++;
+ if (/[A-Z]/.test(password) && /[a-z]/.test(password)) score++;
+ if (/\d/.test(password) && /[^A-Za-z0-9]/.test(password)) score++;
+ if (password.length >= 12) score = Math.min(3, score + 1) as PasswordStrength;
+ return Math.min(3, score) as PasswordStrength;
+}
+
+export function passwordStrengthLabel(s: PasswordStrength): string {
+ return ["Muito fraca", "Fraca", "Média", "Forte"][s];
+}
diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts
index 05e7907..dbc2b0c 100644
--- a/src/routeTree.gen.ts
+++ b/src/routeTree.gen.ts
@@ -10,14 +10,35 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as SignupRouteImport } from './routes/signup'
+import { Route as RedefinirSenhaRouteImport } from './routes/redefinir-senha'
+import { Route as RecuperarSenhaRouteImport } from './routes/recuperar-senha'
+import { Route as PainelRouteImport } from './routes/painel'
import { Route as LoginRouteImport } from './routes/login'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as PainelIndexRouteImport } from './routes/painel.index'
+import { Route as PainelFaturamentoRouteImport } from './routes/painel.faturamento'
+import { Route as PainelConfiguracoesRouteImport } from './routes/painel.configuracoes'
const SignupRoute = SignupRouteImport.update({
id: '/signup',
path: '/signup',
getParentRoute: () => rootRouteImport,
} as any)
+const RedefinirSenhaRoute = RedefinirSenhaRouteImport.update({
+ id: '/redefinir-senha',
+ path: '/redefinir-senha',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const RecuperarSenhaRoute = RecuperarSenhaRouteImport.update({
+ id: '/recuperar-senha',
+ path: '/recuperar-senha',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const PainelRoute = PainelRouteImport.update({
+ id: '/painel',
+ path: '/painel',
+ getParentRoute: () => rootRouteImport,
+} as any)
const LoginRoute = LoginRouteImport.update({
id: '/login',
path: '/login',
@@ -28,34 +49,96 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const PainelIndexRoute = PainelIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => PainelRoute,
+} as any)
+const PainelFaturamentoRoute = PainelFaturamentoRouteImport.update({
+ id: '/faturamento',
+ path: '/faturamento',
+ getParentRoute: () => PainelRoute,
+} as any)
+const PainelConfiguracoesRoute = PainelConfiguracoesRouteImport.update({
+ id: '/configuracoes',
+ path: '/configuracoes',
+ getParentRoute: () => PainelRoute,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/login': typeof LoginRoute
+ '/painel': typeof PainelRouteWithChildren
+ '/recuperar-senha': typeof RecuperarSenhaRoute
+ '/redefinir-senha': typeof RedefinirSenhaRoute
'/signup': typeof SignupRoute
+ '/painel/configuracoes': typeof PainelConfiguracoesRoute
+ '/painel/faturamento': typeof PainelFaturamentoRoute
+ '/painel/': typeof PainelIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/login': typeof LoginRoute
+ '/recuperar-senha': typeof RecuperarSenhaRoute
+ '/redefinir-senha': typeof RedefinirSenhaRoute
'/signup': typeof SignupRoute
+ '/painel/configuracoes': typeof PainelConfiguracoesRoute
+ '/painel/faturamento': typeof PainelFaturamentoRoute
+ '/painel': typeof PainelIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/login': typeof LoginRoute
+ '/painel': typeof PainelRouteWithChildren
+ '/recuperar-senha': typeof RecuperarSenhaRoute
+ '/redefinir-senha': typeof RedefinirSenhaRoute
'/signup': typeof SignupRoute
+ '/painel/configuracoes': typeof PainelConfiguracoesRoute
+ '/painel/faturamento': typeof PainelFaturamentoRoute
+ '/painel/': typeof PainelIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
- fullPaths: '/' | '/login' | '/signup'
+ fullPaths:
+ | '/'
+ | '/login'
+ | '/painel'
+ | '/recuperar-senha'
+ | '/redefinir-senha'
+ | '/signup'
+ | '/painel/configuracoes'
+ | '/painel/faturamento'
+ | '/painel/'
fileRoutesByTo: FileRoutesByTo
- to: '/' | '/login' | '/signup'
- id: '__root__' | '/' | '/login' | '/signup'
+ to:
+ | '/'
+ | '/login'
+ | '/recuperar-senha'
+ | '/redefinir-senha'
+ | '/signup'
+ | '/painel/configuracoes'
+ | '/painel/faturamento'
+ | '/painel'
+ id:
+ | '__root__'
+ | '/'
+ | '/login'
+ | '/painel'
+ | '/recuperar-senha'
+ | '/redefinir-senha'
+ | '/signup'
+ | '/painel/configuracoes'
+ | '/painel/faturamento'
+ | '/painel/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
LoginRoute: typeof LoginRoute
+ PainelRoute: typeof PainelRouteWithChildren
+ RecuperarSenhaRoute: typeof RecuperarSenhaRoute
+ RedefinirSenhaRoute: typeof RedefinirSenhaRoute
SignupRoute: typeof SignupRoute
}
@@ -68,6 +151,27 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SignupRouteImport
parentRoute: typeof rootRouteImport
}
+ '/redefinir-senha': {
+ id: '/redefinir-senha'
+ path: '/redefinir-senha'
+ fullPath: '/redefinir-senha'
+ preLoaderRoute: typeof RedefinirSenhaRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/recuperar-senha': {
+ id: '/recuperar-senha'
+ path: '/recuperar-senha'
+ fullPath: '/recuperar-senha'
+ preLoaderRoute: typeof RecuperarSenhaRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/painel': {
+ id: '/painel'
+ path: '/painel'
+ fullPath: '/painel'
+ preLoaderRoute: typeof PainelRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/login': {
id: '/login'
path: '/login'
@@ -82,14 +186,62 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/painel/': {
+ id: '/painel/'
+ path: '/'
+ fullPath: '/painel/'
+ preLoaderRoute: typeof PainelIndexRouteImport
+ parentRoute: typeof PainelRoute
+ }
+ '/painel/faturamento': {
+ id: '/painel/faturamento'
+ path: '/faturamento'
+ fullPath: '/painel/faturamento'
+ preLoaderRoute: typeof PainelFaturamentoRouteImport
+ parentRoute: typeof PainelRoute
+ }
+ '/painel/configuracoes': {
+ id: '/painel/configuracoes'
+ path: '/configuracoes'
+ fullPath: '/painel/configuracoes'
+ preLoaderRoute: typeof PainelConfiguracoesRouteImport
+ parentRoute: typeof PainelRoute
+ }
}
}
+interface PainelRouteChildren {
+ PainelConfiguracoesRoute: typeof PainelConfiguracoesRoute
+ PainelFaturamentoRoute: typeof PainelFaturamentoRoute
+ PainelIndexRoute: typeof PainelIndexRoute
+}
+
+const PainelRouteChildren: PainelRouteChildren = {
+ PainelConfiguracoesRoute: PainelConfiguracoesRoute,
+ PainelFaturamentoRoute: PainelFaturamentoRoute,
+ PainelIndexRoute: PainelIndexRoute,
+}
+
+const PainelRouteWithChildren =
+ PainelRoute._addFileChildren(PainelRouteChildren)
+
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
LoginRoute: LoginRoute,
+ PainelRoute: PainelRouteWithChildren,
+ RecuperarSenhaRoute: RecuperarSenhaRoute,
+ RedefinirSenhaRoute: RedefinirSenhaRoute,
SignupRoute: SignupRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/react-start'
+declare module '@tanstack/react-start' {
+ interface Register {
+ ssr: true
+ router: Awaited>
+ }
+}
diff --git a/src/router.tsx b/src/router.tsx
index adcf1a9..bde43d8 100644
--- a/src/router.tsx
+++ b/src/router.tsx
@@ -1,4 +1,5 @@
import { createRouter, useRouter } from "@tanstack/react-router";
+import { QueryClient } from "@tanstack/react-query";
import { routeTree } from "./routeTree.gen";
function DefaultErrorComponent({ error, reset }: { error: Error; reset: () => void }) {
@@ -23,9 +24,9 @@ function DefaultErrorComponent({ error, reset }: { error: Error; reset: () => vo
/>
- Something went wrong
+ Algo deu errado
- An unexpected error occurred. Please try again.
+ Ocorreu um erro inesperado. Tente novamente.
{import.meta.env.DEV && error.message && (
@@ -38,15 +39,15 @@ function DefaultErrorComponent({ error, reset }: { error: Error; reset: () => vo
router.invalidate();
reset();
}}
- className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
+ className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary-dark"
>
- Try again
+ Tentar novamente
- Go home
+ Ir para o início
@@ -55,9 +56,20 @@ function DefaultErrorComponent({ error, reset }: { error: Error; reset: () => vo
}
export const getRouter = () => {
+ // Fresh QueryClient per request — never module-level (vaza dados entre SSR requests)
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 30_000,
+ retry: 1,
+ refetchOnWindowFocus: false,
+ },
+ },
+ });
+
const router = createRouter({
routeTree,
- context: {},
+ context: { queryClient },
scrollRestoration: true,
defaultPreloadStaleTime: 0,
defaultErrorComponent: DefaultErrorComponent,
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index ad8467f..3605010 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -1,9 +1,15 @@
-import { Outlet, createRootRoute, HeadContent, Scripts } from "@tanstack/react-router";
+import { Outlet, createRootRouteWithContext, HeadContent, Scripts, Link } from "@tanstack/react-router";
+import type { QueryClient } from "@tanstack/react-query";
+import { QueryClientProvider } from "@tanstack/react-query";
import { Toaster } from "@/components/ui/sonner";
import { ThemeProvider } from "@/components/theme-provider";
import appCss from "../styles.css?url";
+interface RouterContext {
+ queryClient: QueryClient;
+}
+
function NotFoundComponent() {
return (
@@ -14,19 +20,19 @@ function NotFoundComponent() {
A página que você procura não existe ou foi movida.
);
}
-export const Route = createRootRoute({
+export const Route = createRootRouteWithContext()({
head: () => ({
meta: [
{ charSet: "utf-8" },
@@ -88,10 +94,13 @@ function RootShell({ children }: { children: React.ReactNode }) {
}
function RootComponent() {
+ const { queryClient } = Route.useRouteContext();
return (
-
-
-
-
+
+
+
+
+
+
);
}
diff --git a/src/routes/login.tsx b/src/routes/login.tsx
index 8f48862..269ffd6 100644
--- a/src/routes/login.tsx
+++ b/src/routes/login.tsx
@@ -1,21 +1,106 @@
-import { createFileRoute, Link } from "@tanstack/react-router";
-import { Logo } from "@/components/mika/Logo";
+"use client";
+
+import { useState } from "react";
+import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import { toast } from "sonner";
+import { Loader2 } from "lucide-react";
+import { supabase } from "@/integrations/supabase/client";
+import { translateAuthError } from "@/lib/auth-errors";
+import { AuthCard } from "@/components/mika/AuthCard";
+import { GoogleButton } from "@/components/mika/GoogleButton";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+
+type LoginSearch = { redirect?: string };
+
+const schema = z.object({
+ email: z.string().email("Formato de e-mail inválido."),
+ password: z.string().min(1, "Informe sua senha."),
+});
+type FormValues = z.infer;
export const Route = createFileRoute("/login")({
- component: LoginPlaceholder,
+ validateSearch: (search: Record): LoginSearch => ({
+ redirect: typeof search.redirect === "string" ? search.redirect : undefined,
+ }),
+ component: LoginPage,
});
-function LoginPlaceholder() {
+function LoginPage() {
+ const { redirect } = Route.useSearch();
+ const navigate = useNavigate();
+ const [submitting, setSubmitting] = useState(false);
+ const { register, handleSubmit, formState: { errors } } = useForm({
+ resolver: zodResolver(schema),
+ });
+
+ const onSubmit = async (data: FormValues) => {
+ setSubmitting(true);
+ const { error } = await supabase.auth.signInWithPassword({
+ email: data.email,
+ password: data.password,
+ });
+ setSubmitting(false);
+ if (error) {
+ toast.error(translateAuthError(error.message));
+ return;
+ }
+ toast.success("Bem-vindo de volta!");
+ navigate({ to: (redirect as "/painel") || "/painel" });
+ };
+
return (
-
-
-
-
Login
-
- A página de login completa será implementada na Etapa 2 (Auth + Supabase).
-
-
← Voltar para o início
+
+ Não tem conta?{" "}
+
+ Criar conta
+
+ >
+ }
+ >
+
+
+
-
+
+
+
);
}
diff --git a/src/routes/painel.configuracoes.tsx b/src/routes/painel.configuracoes.tsx
new file mode 100644
index 0000000..9be147e
--- /dev/null
+++ b/src/routes/painel.configuracoes.tsx
@@ -0,0 +1,159 @@
+"use client";
+
+import { createFileRoute } from "@tanstack/react-router";
+import { useState, useEffect } from "react";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { IMaskInput } from "react-imask";
+import { toast } from "sonner";
+import { Loader2, LogOut } from "lucide-react";
+import { supabase } from "@/integrations/supabase/client";
+import { useAuth } from "@/hooks/use-auth";
+import { useProfile } from "@/hooks/use-profile";
+import { translateAuthError } from "@/lib/auth-errors";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export const Route = createFileRoute("/painel/configuracoes")({
+ component: SettingsPage,
+});
+
+const profileSchema = z.object({
+ full_name: z.string().min(2, "Informe seu nome completo.").max(120),
+ company_name: z.string().max(120).optional().or(z.literal("")),
+ cpf_cnpj: z.string().optional().or(z.literal("")),
+ phone: z.string().optional().or(z.literal("")),
+});
+type ProfileForm = z.infer
;
+
+function SettingsPage() {
+ const { user } = useAuth();
+ const { data: profile, isLoading } = useProfile();
+ const queryClient = useQueryClient();
+
+ const { register, handleSubmit, setValue, watch, reset, formState: { errors } } = useForm({
+ resolver: zodResolver(profileSchema),
+ });
+
+ useEffect(() => {
+ if (profile) {
+ reset({
+ full_name: profile.full_name,
+ company_name: profile.company_name ?? "",
+ cpf_cnpj: profile.cpf_cnpj ?? "",
+ phone: profile.phone ?? "",
+ });
+ }
+ }, [profile, reset]);
+
+ const cpfCnpj = watch("cpf_cnpj") || "";
+ const phone = watch("phone") || "";
+ const cpfCnpjDigits = cpfCnpj.replace(/\D/g, "");
+ const cpfCnpjMask = cpfCnpjDigits.length > 11 ? "00.000.000/0000-00" : "000.000.000-00";
+
+ const updateProfile = useMutation({
+ mutationFn: async (data: ProfileForm) => {
+ if (!user) throw new Error("Sem sessão");
+ const { error } = await supabase
+ .from("profiles")
+ .update({
+ full_name: data.full_name,
+ company_name: data.company_name || null,
+ cpf_cnpj: data.cpf_cnpj || null,
+ phone: data.phone || null,
+ })
+ .eq("id", user.id);
+ if (error) throw error;
+ },
+ onSuccess: () => {
+ toast.success("Perfil atualizado com sucesso.");
+ queryClient.invalidateQueries({ queryKey: ["profile"] });
+ },
+ onError: (e: Error) => toast.error(translateAuthError(e.message)),
+ });
+
+ const signOutAll = async () => {
+ const { error } = await supabase.auth.signOut({ scope: "global" });
+ if (error) toast.error(translateAuthError(error.message));
+ else toast.success("Saiu de todos os dispositivos.");
+ };
+
+ if (isLoading) return ;
+
+ return (
+
+
+
+
+ Perfil
+ Informações para faturamento e suporte.
+
+
+
+
+
+ Segurança
+
+ Para alterar sua senha, use o link de recuperação no e-mail.
+
+
+
+ Sair de todos os dispositivos
+
+
+
+ );
+}
diff --git a/src/routes/painel.faturamento.tsx b/src/routes/painel.faturamento.tsx
new file mode 100644
index 0000000..331d81d
--- /dev/null
+++ b/src/routes/painel.faturamento.tsx
@@ -0,0 +1,127 @@
+"use client";
+
+import { createFileRoute } from "@tanstack/react-router";
+import { format } from "date-fns";
+import { ptBR } from "date-fns/locale";
+import { CreditCard, ExternalLink } from "lucide-react";
+import { useSubscription, useProfile } from "@/hooks/use-profile";
+import { useQuery } from "@tanstack/react-query";
+import { supabase } from "@/integrations/supabase/client";
+import { Button } from "@/components/ui/button";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export const Route = createFileRoute("/painel/faturamento")({
+ component: BillingPage,
+});
+
+function BillingPage() {
+ const { data: subscription, isLoading } = useSubscription();
+ const { data: profile } = useProfile();
+
+ const { data: plan } = useQuery({
+ queryKey: ["plan", subscription?.plan_id],
+ enabled: !!subscription?.plan_id,
+ queryFn: async () => {
+ const { data } = await supabase
+ .from("plans")
+ .select("*")
+ .eq("id", subscription!.plan_id!)
+ .maybeSingle();
+ return data;
+ },
+ });
+
+ return (
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+ Plano atual
+ {subscription && plan ? (
+
+
+
+
+
+
+ ) : (
+
+ Você ainda não possui uma assinatura ativa.
+
+ )}
+
+
+ Gerenciar assinatura
+
+
+ )}
+
+
+ Histórico de pagamentos
+
+ Seu histórico aparecerá aqui após a primeira cobrança.
+
+
+
+
+ Método de pagamento
+
+
+ Nenhum método cadastrado
+
+
+ Trocar método
+
+
+
+ );
+}
+
+function Field({ label, value }: { label: string; value: string }) {
+ return (
+
+ );
+}
+
+function statusLabel(s: string): string {
+ return {
+ active: "Ativa",
+ trialing: "Em período de teste",
+ past_due: "Pagamento pendente",
+ canceled: "Cancelada",
+ incomplete: "Em provisionamento",
+ incomplete_expired: "Expirada",
+ unpaid: "Não paga",
+ }[s] || s;
+}
diff --git a/src/routes/painel.index.tsx b/src/routes/painel.index.tsx
new file mode 100644
index 0000000..e339ea9
--- /dev/null
+++ b/src/routes/painel.index.tsx
@@ -0,0 +1,132 @@
+"use client";
+
+import { createFileRoute, Link } from "@tanstack/react-router";
+import { ArrowRight, CheckCircle2, Loader2, Sparkles } from "lucide-react";
+import { useSubscription } from "@/hooks/use-profile";
+import { useProfile } from "@/hooks/use-profile";
+import { Button } from "@/components/ui/button";
+import { Skeleton } from "@/components/ui/skeleton";
+import { SubscriptionBanner } from "@/components/mika/SubscriptionBanner";
+import { cn } from "@/lib/utils";
+
+export const Route = createFileRoute("/painel/")({
+ component: DashboardPage,
+});
+
+function DashboardPage() {
+ const { data: subscription, isLoading } = useSubscription();
+ const { data: profile } = useProfile();
+
+ if (isLoading) {
+ return (
+
+
+
+
+ );
+ }
+
+ const firstName = (profile?.full_name || "").split(" ")[0] || "por aqui";
+
+ return (
+
+
+
+
+
+ {!subscription &&
}
+
+ {subscription && (subscription.status === "incomplete" || subscription.status === "active") && (
+
+ )}
+
+ );
+}
+
+function NoSubscriptionCard() {
+ return (
+
+
+
+
+
Escolha um plano para começar
+
+ Em poucos minutos seu agente Mika estará disponível no Telegram, com memória persistente e
+ skills personalizadas.
+
+
+
+ Ver planos
+
+
+
+ );
+}
+
+function ProvisioningCard() {
+ const steps = [
+ { label: "Provisionar container na VPS", state: "active" as const },
+ { label: "Configurar modelo de IA", state: "pending" as const },
+ { label: "Conectar seu Telegram", state: "pending" as const },
+ { label: "Personalizar seu agente", state: "pending" as const },
+ ];
+ return (
+
+
+
+
+
+
+
Seu agente Mika está sendo provisionado
+
+ Você receberá um e-mail quando estiver pronto — geralmente em até 10 minutos.
+
+
+
+
+
+ {steps.map((step, i) => (
+ -
+
+
+ {step.state === "active" ? (
+
+ ) : step.state === "pending" ? (
+ i + 1
+ ) : (
+
+ )}
+
+
+ {step.label}
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/routes/painel.tsx b/src/routes/painel.tsx
new file mode 100644
index 0000000..f2df24b
--- /dev/null
+++ b/src/routes/painel.tsx
@@ -0,0 +1,237 @@
+"use client";
+
+import { createFileRoute, Outlet, redirect, Link, useNavigate, useLocation } from "@tanstack/react-router";
+import { useEffect, useState } from "react";
+import {
+ Bot,
+ CreditCard,
+ Home,
+ LogOut,
+ Menu,
+ Plug,
+ Settings,
+ Sparkles,
+ User,
+} from "lucide-react";
+import { supabase } from "@/integrations/supabase/client";
+import { useAuth } from "@/hooks/use-auth";
+import { useProfile } from "@/hooks/use-profile";
+import { Logo } from "@/components/mika/Logo";
+import { ThemeToggle } from "@/components/mika/ThemeToggle";
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import {
+ Sheet,
+ SheetContent,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger,
+} from "@/components/ui/sheet";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { cn } from "@/lib/utils";
+
+export const Route = createFileRoute("/painel")({
+ beforeLoad: () => {
+ // Auth real é validada client-side abaixo (Supabase usa localStorage).
+ // Mantemos beforeLoad como hook para futuro SSR auth.
+ },
+ component: PainelLayout,
+});
+
+interface NavItem {
+ to: "/painel" | "/painel/faturamento" | "/painel/configuracoes";
+ label: string;
+ icon: React.ComponentType<{ className?: string }>;
+ disabled?: boolean;
+}
+
+interface DisabledNavItem {
+ to: null;
+ label: string;
+ icon: React.ComponentType<{ className?: string }>;
+ disabled: true;
+}
+
+const NAV: (NavItem | DisabledNavItem)[] = [
+ { to: "/painel", label: "Dashboard", icon: Home },
+ { to: null, label: "Meu Agente", icon: Bot, disabled: true },
+ { to: null, label: "Skills", icon: Sparkles, disabled: true },
+ { to: null, label: "Integrações", icon: Plug, disabled: true },
+ { to: "/painel/faturamento", label: "Faturamento", icon: CreditCard },
+ { to: "/painel/configuracoes", label: "Configurações", icon: Settings },
+];
+
+function PainelLayout() {
+ const { user, loading } = useAuth();
+ const navigate = useNavigate();
+ const location = useLocation();
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ useEffect(() => {
+ if (!loading && !user) {
+ navigate({ to: "/login", search: { redirect: location.pathname } });
+ }
+ }, [loading, user, navigate, location.pathname]);
+
+ if (loading || !user) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setMobileOpen(false)} />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function DesktopSidebar() {
+ return (
+
+ );
+}
+
+function SidebarNav({ onNavigate }: { onNavigate?: () => void } = {}) {
+ const location = useLocation();
+ return (
+
+ );
+}
+
+function UserMenu() {
+ const { user } = useAuth();
+ const { data: profile } = useProfile();
+ const navigate = useNavigate();
+
+ const initials = (profile?.full_name || user?.email || "U")
+ .split(" ")
+ .map((s) => s[0])
+ .slice(0, 2)
+ .join("")
+ .toUpperCase();
+
+ const signOut = async () => {
+ await supabase.auth.signOut();
+ navigate({ to: "/" });
+ };
+
+ return (
+
+
+
+
+
+
+ {profile?.avatar_url && }
+
+ {initials}
+
+
+
+
+
+
+ {profile?.full_name || "Usuário"}
+ {user?.email}
+
+
+
+
+ Perfil
+
+
+
+ Sair
+
+
+
+
+ );
+}
diff --git a/src/routes/recuperar-senha.tsx b/src/routes/recuperar-senha.tsx
new file mode 100644
index 0000000..6acfe58
--- /dev/null
+++ b/src/routes/recuperar-senha.tsx
@@ -0,0 +1,86 @@
+"use client";
+
+import { useState } from "react";
+import { createFileRoute, Link } from "@tanstack/react-router";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import { toast } from "sonner";
+import { Loader2 } from "lucide-react";
+import { supabase } from "@/integrations/supabase/client";
+import { translateAuthError } from "@/lib/auth-errors";
+import { AuthCard } from "@/components/mika/AuthCard";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+
+const schema = z.object({
+ email: z.string().email("Formato de e-mail inválido."),
+});
+type FormValues = z.infer;
+
+export const Route = createFileRoute("/recuperar-senha")({
+ component: ForgotPasswordPage,
+});
+
+function ForgotPasswordPage() {
+ const [submitting, setSubmitting] = useState(false);
+ const [sent, setSent] = useState(false);
+ const { register, handleSubmit, formState: { errors } } = useForm({
+ resolver: zodResolver(schema),
+ });
+
+ const onSubmit = async (data: FormValues) => {
+ setSubmitting(true);
+ const { error } = await supabase.auth.resetPasswordForEmail(data.email, {
+ redirectTo: `${window.location.origin}/redefinir-senha`,
+ });
+ setSubmitting(false);
+ if (error) {
+ toast.error(translateAuthError(error.message));
+ return;
+ }
+ setSent(true);
+ toast.success("Se houver uma conta com esse e-mail, enviamos um link de recuperação.");
+ };
+
+ return (
+
+ ← Voltar para o login
+
+ }
+ >
+ {sent ? (
+
+
+ Se o e-mail informado existir em nossa base, você receberá o link em alguns minutos.
+ Verifique também a pasta de spam.
+
+
+ Voltar para o login
+
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/src/routes/redefinir-senha.tsx b/src/routes/redefinir-senha.tsx
new file mode 100644
index 0000000..67c1c94
--- /dev/null
+++ b/src/routes/redefinir-senha.tsx
@@ -0,0 +1,90 @@
+"use client";
+
+import { useState } from "react";
+import { createFileRoute, useNavigate } from "@tanstack/react-router";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import { toast } from "sonner";
+import { Loader2 } from "lucide-react";
+import { supabase } from "@/integrations/supabase/client";
+import { translateAuthError } from "@/lib/auth-errors";
+import { passwordStrength } from "@/lib/password";
+import { AuthCard } from "@/components/mika/AuthCard";
+import { PasswordStrengthMeter } from "@/components/mika/PasswordStrengthMeter";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+
+const schema = z
+ .object({
+ password: z
+ .string()
+ .min(8, "A senha precisa de pelo menos 8 caracteres, 1 maiúscula e 1 número.")
+ .refine((v) => /[A-Z]/.test(v), "A senha precisa de pelo menos 1 maiúscula.")
+ .refine((v) => /\d/.test(v), "A senha precisa de pelo menos 1 número."),
+ confirm: z.string(),
+ })
+ .refine((d) => d.password === d.confirm, {
+ path: ["confirm"],
+ message: "As senhas não coincidem.",
+ });
+type FormValues = z.infer;
+
+export const Route = createFileRoute("/redefinir-senha")({
+ component: ResetPasswordPage,
+});
+
+function ResetPasswordPage() {
+ const navigate = useNavigate();
+ const [submitting, setSubmitting] = useState(false);
+ const { register, handleSubmit, watch, formState: { errors } } = useForm({
+ resolver: zodResolver(schema),
+ });
+ const password = watch("password") || "";
+
+ const onSubmit = async (data: FormValues) => {
+ if (passwordStrength(data.password) < 2) {
+ toast.error("Escolha uma senha mais forte.");
+ return;
+ }
+ setSubmitting(true);
+ const { error } = await supabase.auth.updateUser({ password: data.password });
+ setSubmitting(false);
+ if (error) {
+ toast.error(translateAuthError(error.message));
+ return;
+ }
+ toast.success("Senha redefinida com sucesso. Faça login.");
+ await supabase.auth.signOut();
+ navigate({ to: "/login" });
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/src/routes/signup.tsx b/src/routes/signup.tsx
index 848ccdd..6536f40 100644
--- a/src/routes/signup.tsx
+++ b/src/routes/signup.tsx
@@ -1,38 +1,215 @@
-import { createFileRoute, Link } from "@tanstack/react-router";
-import { Logo } from "@/components/mika/Logo";
+"use client";
+
+import { useState } from "react";
+import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import { toast } from "sonner";
+import { Loader2, MailCheck } from "lucide-react";
+import { supabase } from "@/integrations/supabase/client";
+import { translateAuthError } from "@/lib/auth-errors";
+import { passwordStrength } from "@/lib/password";
+import { AuthCard } from "@/components/mika/AuthCard";
+import { GoogleButton } from "@/components/mika/GoogleButton";
+import { PasswordStrengthMeter } from "@/components/mika/PasswordStrengthMeter";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Checkbox } from "@/components/ui/checkbox";
type SignupSearch = {
plan?: string;
cycle?: "monthly" | "yearly";
};
+const schema = z.object({
+ full_name: z.string().min(2, "Informe seu nome completo.").max(120),
+ email: z.string().email("Formato de e-mail inválido."),
+ password: z
+ .string()
+ .min(8, "A senha precisa de pelo menos 8 caracteres, 1 maiúscula e 1 número.")
+ .refine((v) => /[A-Z]/.test(v), "A senha precisa de pelo menos 1 maiúscula.")
+ .refine((v) => /\d/.test(v), "A senha precisa de pelo menos 1 número."),
+ accept_terms: z.literal(true, { message: "Você precisa aceitar os termos." }),
+});
+type FormValues = z.infer;
+
export const Route = createFileRoute("/signup")({
validateSearch: (search: Record): SignupSearch => ({
plan: typeof search.plan === "string" ? search.plan : undefined,
cycle: search.cycle === "yearly" || search.cycle === "monthly" ? search.cycle : undefined,
}),
- component: SignupPlaceholder,
+ component: SignupPage,
});
-function SignupPlaceholder() {
+function SignupPage() {
const { plan, cycle } = Route.useSearch();
- return (
-
-
-
-
Criar conta
-
- Cadastro completo será implementado na Etapa 2.
-
- {plan && (
-
- Plano selecionado: {plan} · {cycle === "yearly" ? "anual" : "mensal"}
+ const navigate = useNavigate();
+ const [submitting, setSubmitting] = useState(false);
+ const [needsConfirm, setNeedsConfirm] = useState(null);
+
+ const {
+ register,
+ handleSubmit,
+ setValue,
+ watch,
+ formState: { errors },
+ } = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: { accept_terms: false as unknown as true },
+ });
+
+ const password = watch("password") || "";
+
+ const onSubmit = async (data: FormValues) => {
+ if (passwordStrength(data.password) < 2) {
+ toast.error("Escolha uma senha mais forte.");
+ return;
+ }
+ setSubmitting(true);
+ const { data: signupData, error } = await supabase.auth.signUp({
+ email: data.email,
+ password: data.password,
+ options: {
+ data: { full_name: data.full_name },
+ emailRedirectTo: `${window.location.origin}/painel`,
+ },
+ });
+ setSubmitting(false);
+ if (error) {
+ toast.error(translateAuthError(error.message));
+ return;
+ }
+ // Sessão presente = e-mail já confirmado (auto-confirm desligado, então normalmente null)
+ if (signupData.session) {
+ toast.success("Conta criada!");
+ if (plan && plan !== "enterprise") {
+ // TODO Etapa 3: chamar create-checkout-session com plan/cycle
+ navigate({ to: "/painel" });
+ } else {
+ navigate({ to: "/painel" });
+ }
+ return;
+ }
+ setNeedsConfirm(data.email);
+ };
+
+ const resend = async () => {
+ if (!needsConfirm) return;
+ const { error } = await supabase.auth.resend({ type: "signup", email: needsConfirm });
+ if (error) {
+ toast.error(translateAuthError(error.message));
+ return;
+ }
+ toast.success("E-mail reenviado. Confira sua caixa de entrada.");
+ };
+
+ if (needsConfirm) {
+ return (
+
+
+
+
+
+
+ Clique no link do e-mail para ativar sua conta. Não esqueça de checar a pasta de spam.
- )}
-
- ← Voltar para o início
+
+ Reenviar e-mail de confirmação
+
+
+ Voltar para o login
+
+
+ );
+ }
+
+ return (
+
+ Já tem conta?{" "}
+
+ Entrar
+
+ >
+ }
+ >
+
+
+
-
+
+
+
);
}
diff --git a/supabase/migrations/20260417170038_befdbd7a-ab3b-44a1-89d7-e7aead3d791d.sql b/supabase/migrations/20260417170038_befdbd7a-ab3b-44a1-89d7-e7aead3d791d.sql
new file mode 100644
index 0000000..41fb2a2
--- /dev/null
+++ b/supabase/migrations/20260417170038_befdbd7a-ab3b-44a1-89d7-e7aead3d791d.sql
@@ -0,0 +1,210 @@
+
+-- ============================================
+-- HELPER: trigger para updated_at
+-- ============================================
+CREATE OR REPLACE FUNCTION public.update_updated_at_column()
+RETURNS TRIGGER
+LANGUAGE plpgsql
+SET search_path = public
+AS $$
+BEGIN
+ NEW.updated_at = now();
+ RETURN NEW;
+END;
+$$;
+
+-- ============================================
+-- PROFILES
+-- ============================================
+CREATE TABLE public.profiles (
+ id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
+ full_name TEXT NOT NULL DEFAULT '',
+ company_name TEXT,
+ cpf_cnpj TEXT,
+ phone TEXT,
+ avatar_url TEXT,
+ stripe_customer_id TEXT UNIQUE,
+ onboarding_completed BOOLEAN NOT NULL DEFAULT false,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE INDEX idx_profiles_stripe_customer_id ON public.profiles(stripe_customer_id);
+
+ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "Usuários veem o próprio perfil"
+ ON public.profiles FOR SELECT
+ USING (auth.uid() = id);
+
+CREATE POLICY "Usuários atualizam o próprio perfil"
+ ON public.profiles FOR UPDATE
+ USING (auth.uid() = id);
+
+CREATE POLICY "Usuários inserem o próprio perfil"
+ ON public.profiles FOR INSERT
+ WITH CHECK (auth.uid() = id);
+
+CREATE TRIGGER update_profiles_updated_at
+ BEFORE UPDATE ON public.profiles
+ FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
+
+-- Trigger: cria profile automaticamente no signup
+CREATE OR REPLACE FUNCTION public.handle_new_user()
+RETURNS TRIGGER
+LANGUAGE plpgsql
+SECURITY DEFINER
+SET search_path = public
+AS $$
+BEGIN
+ INSERT INTO public.profiles (id, full_name)
+ VALUES (
+ NEW.id,
+ COALESCE(NEW.raw_user_meta_data->>'full_name', '')
+ );
+ RETURN NEW;
+END;
+$$;
+
+CREATE TRIGGER on_auth_user_created
+ AFTER INSERT ON auth.users
+ FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
+
+-- ============================================
+-- PLANS
+-- ============================================
+CREATE TABLE public.plans (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ slug TEXT UNIQUE NOT NULL,
+ name TEXT NOT NULL,
+ description TEXT,
+ price_monthly_brl NUMERIC(10,2),
+ price_yearly_brl NUMERIC(10,2),
+ stripe_price_id_monthly TEXT,
+ stripe_price_id_yearly TEXT,
+ features JSONB NOT NULL DEFAULT '[]'::jsonb,
+ highlighted BOOLEAN NOT NULL DEFAULT false,
+ is_enterprise BOOLEAN NOT NULL DEFAULT false,
+ display_order INT NOT NULL DEFAULT 0,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+ALTER TABLE public.plans ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "Planos são públicos"
+ ON public.plans FOR SELECT
+ USING (true);
+
+INSERT INTO public.plans (slug, name, description, price_monthly_brl, price_yearly_brl, features, highlighted, is_enterprise, display_order) VALUES
+('basic', 'Basic', 'Para quem está começando a explorar IA pessoal', 69.90, 671.04,
+ '["Memória persistente básica","Até 500 mensagens/mês","Integração com Telegram","Suporte por e-mail"]'::jsonb,
+ false, false, 1),
+('starter', 'Starter', 'Para profissionais que querem produtividade real', 199.90, 1919.04,
+ '["Memória persistente avançada","Até 3.000 mensagens/mês","Integração Google Workspace","Skills personalizadas (até 10)","Suporte prioritário"]'::jsonb,
+ false, false, 2),
+('professional', 'Professional', 'Para quem usa IA o dia inteiro', 399.90, 3839.04,
+ '["Memória persistente ilimitada","Mensagens ilimitadas","Skills ilimitadas","Agendamentos automáticos","Integrações premium","Suporte dedicado em horário comercial"]'::jsonb,
+ true, false, 3),
+('enterprise', 'Enterprise', 'Para times e empresas', NULL, NULL,
+ '["Tudo do Professional","Múltiplos usuários","SSO e auditoria","SLA garantido","Onboarding personalizado","Gerente de conta dedicado"]'::jsonb,
+ false, true, 4);
+
+-- ============================================
+-- SUBSCRIPTIONS
+-- ============================================
+CREATE TABLE public.subscriptions (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
+ plan_id UUID REFERENCES public.plans(id),
+ stripe_subscription_id TEXT UNIQUE,
+ status TEXT NOT NULL CHECK (status IN ('active','trialing','past_due','canceled','incomplete','incomplete_expired','unpaid')),
+ billing_cycle TEXT NOT NULL CHECK (billing_cycle IN ('monthly','yearly')),
+ current_period_start TIMESTAMPTZ,
+ current_period_end TIMESTAMPTZ,
+ cancel_at_period_end BOOLEAN NOT NULL DEFAULT false,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE INDEX idx_subscriptions_user_id ON public.subscriptions(user_id);
+CREATE INDEX idx_subscriptions_stripe_subscription_id ON public.subscriptions(stripe_subscription_id);
+CREATE INDEX idx_subscriptions_status ON public.subscriptions(status);
+
+ALTER TABLE public.subscriptions ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "Usuários veem a própria assinatura"
+ ON public.subscriptions FOR SELECT
+ USING (auth.uid() = user_id);
+
+CREATE TRIGGER update_subscriptions_updated_at
+ BEFORE UPDATE ON public.subscriptions
+ FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
+
+-- ============================================
+-- AGENT INSTANCES
+-- ============================================
+CREATE TABLE public.agent_instances (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id UUID UNIQUE NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
+ status TEXT NOT NULL DEFAULT 'provisioning' CHECK (status IN ('provisioning','active','suspended','error')),
+ vps_host TEXT,
+ container_name TEXT,
+ telegram_bot_token_vault_id UUID,
+ telegram_bot_username TEXT,
+ uuid_tenant UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE INDEX idx_agent_instances_user_id ON public.agent_instances(user_id);
+CREATE INDEX idx_agent_instances_uuid_tenant ON public.agent_instances(uuid_tenant);
+CREATE INDEX idx_agent_instances_status ON public.agent_instances(status);
+
+ALTER TABLE public.agent_instances ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "Usuários veem o próprio agente"
+ ON public.agent_instances FOR SELECT
+ USING (auth.uid() = user_id);
+
+CREATE TRIGGER update_agent_instances_updated_at
+ BEFORE UPDATE ON public.agent_instances
+ FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
+
+-- ============================================
+-- STRIPE WEBHOOK EVENTS (idempotência)
+-- ============================================
+CREATE TABLE public.stripe_webhook_events (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ stripe_event_id TEXT UNIQUE NOT NULL,
+ event_type TEXT NOT NULL,
+ processed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ payload JSONB
+);
+
+CREATE INDEX idx_stripe_webhook_events_event_id ON public.stripe_webhook_events(stripe_event_id);
+CREATE INDEX idx_stripe_webhook_events_event_type ON public.stripe_webhook_events(event_type);
+
+ALTER TABLE public.stripe_webhook_events ENABLE ROW LEVEL SECURITY;
+-- Sem policies: apenas service_role acessa
+
+-- ============================================
+-- ENTERPRISE LEADS
+-- ============================================
+CREATE TABLE public.enterprise_leads (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ company_name TEXT NOT NULL,
+ contact_name TEXT NOT NULL,
+ email TEXT NOT NULL,
+ phone TEXT,
+ team_size TEXT NOT NULL CHECK (team_size IN ('1-10','11-50','51-200','200+')),
+ message TEXT,
+ status TEXT NOT NULL DEFAULT 'new' CHECK (status IN ('new','contacted','qualified','converted','lost')),
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+ALTER TABLE public.enterprise_leads ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "Qualquer pessoa pode enviar lead"
+ ON public.enterprise_leads FOR INSERT
+ WITH CHECK (true);
+-- Leitura: apenas service_role
diff --git a/supabase/migrations/20260417170055_029831ee-4fe4-41ae-bf04-f7875b5fdf31.sql b/supabase/migrations/20260417170055_029831ee-4fe4-41ae-bf04-f7875b5fdf31.sql
new file mode 100644
index 0000000..4239d35
--- /dev/null
+++ b/supabase/migrations/20260417170055_029831ee-4fe4-41ae-bf04-f7875b5fdf31.sql
@@ -0,0 +1,15 @@
+
+DROP POLICY IF EXISTS "Qualquer pessoa pode enviar lead" ON public.enterprise_leads;
+
+CREATE POLICY "Visitantes podem enviar lead"
+ ON public.enterprise_leads FOR INSERT
+ TO anon, authenticated
+ WITH CHECK (
+ char_length(company_name) BETWEEN 1 AND 200
+ AND char_length(contact_name) BETWEEN 1 AND 200
+ AND char_length(email) BETWEEN 3 AND 320
+ AND email ~* '^[^@\s]+@[^@\s]+\.[^@\s]+$'
+ AND team_size IN ('1-10','11-50','51-200','200+')
+ AND (message IS NULL OR char_length(message) <= 2000)
+ AND status = 'new'
+ );