diff --git a/.env.local.template b/.env.local.template
new file mode 100644
index 0000000..910d4dd
--- /dev/null
+++ b/.env.local.template
@@ -0,0 +1,6 @@
+# Supabase
+NEXT_PUBLIC_SUPABASE_URL="YOUR_SUPABASE_PROJECT_URL"
+NEXT_PUBLIC_SUPABASE_ANON_KEY="YOUR_SUPABASE_ANON_KEY"
+
+# Resend (Email Service)
+RESEND_API_KEY="YOUR_RESEND_API_KEY"
diff --git a/package.json b/package.json
index 0ebfd75..b46ce58 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,8 @@
"supabase:types": "supabase gen types typescript > src/types/supabase.ts"
},
"dependencies": {
+ "@react-email/components": "^1.0.8",
+ "@react-email/render": "^2.0.4",
"@serwist/next": "^9.5.6",
"@supabase/ssr": "^0.8.0",
"@supabase/supabase-js": "^2.39.0",
@@ -20,7 +22,9 @@
"lucide-react": "^0.575.0",
"next": "16.1.6",
"react": "19.2.3",
+ "react-colorful": "^5.6.1",
"react-dom": "19.2.3",
+ "resend": "^6.9.2",
"serwist": "^9.5.6",
"tailwind-merge": "^2.2.1"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 170a375..c36388c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,12 @@ importers:
.:
dependencies:
+ '@react-email/components':
+ specifier: ^1.0.8
+ version: 1.0.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@react-email/render':
+ specifier: ^2.0.4
+ version: 2.0.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@serwist/next':
specifier: ^9.5.6
version: 9.5.6(next@16.1.6(@babel/core@7.29.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(typescript@5.9.3)
@@ -32,9 +38,15 @@ importers:
react:
specifier: 19.2.3
version: 19.2.3
+ react-colorful:
+ specifier: ^5.6.1
+ version: 5.6.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react-dom:
specifier: 19.2.3
version: 19.2.3(react@19.2.3)
+ resend:
+ specifier: ^6.9.2
+ version: 6.9.2(@react-email/render@2.0.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))
serwist:
specifier: ^9.5.6
version: 9.5.6(browserslist@4.28.1)(typescript@5.9.3)
@@ -450,9 +462,171 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
+ '@react-email/body@0.2.1':
+ resolution: {integrity: sha512-ljDiQiJDu/Fq//vSIIP0z5Nuvt4+DX1RqGasstChDGJB/14ogd4VdNS9aacoede/ZjGy3o3Qb+cxyS+XgM6SwQ==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+
+ '@react-email/button@0.2.1':
+ resolution: {integrity: sha512-qXyj7RZLE7POy9BMKSoqQ00tOXThjOZSUnI2Yu9i29IHngPlmrNayIWBoVKtElES7OWwypUcpiajwi1mUWx6/A==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+
+ '@react-email/code-block@0.2.1':
+ resolution: {integrity: sha512-M3B7JpVH4ytgn83/ujRR1k1DQHvTeABiDM61OvAbjLRPhC/5KLHU5KkzIbbuGIrjWwxAbL1kSQzU8MhLEtSxyw==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+
+ '@react-email/code-inline@0.0.6':
+ resolution: {integrity: sha512-jfhebvv3dVsp3OdPgKXnk8+e2pBiDVZejDOBFzBa/IblrAJ9cQDkN6rBD5IyEg8hTOxwbw3iaI/yZFmDmIguIA==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+
+ '@react-email/column@0.0.14':
+ resolution: {integrity: sha512-f+W+Bk2AjNO77zynE33rHuQhyqVICx4RYtGX9NKsGUg0wWjdGP0qAuIkhx9Rnmk4/hFMo1fUrtYNqca9fwJdHg==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+
+ '@react-email/components@1.0.8':
+ resolution: {integrity: sha512-zY81ED6o5MWMzBkr9uZFuT24lWarT+xIbOZxI6C9dsFmCWBczM8IE1BgOI8rhpUK4JcYVDy1uKxYAFqsx2Bc4w==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+
+ '@react-email/container@0.0.16':
+ resolution: {integrity: sha512-QWBB56RkkU0AJ9h+qy33gfT5iuZknPC7Un/IjZv9B0QmMIK+WWacc0cH6y2SV5Cv/b99hU94fjEMOOO4enpkbQ==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+
+ '@react-email/font@0.0.10':
+ resolution: {integrity: sha512-0urVSgCmQIfx5r7Xc586miBnQUVnGp3OTYUm8m5pwtQRdTRO5XrTtEfNJ3JhYhSOruV0nD8fd+dXtKXobum6tA==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+
+ '@react-email/head@0.0.13':
+ resolution: {integrity: sha512-AJg6le/08Gz4tm+6MtKXqtNNyKHzmooOCdmtqmWxD7FxoAdU1eVcizhtQ0gcnVaY6ethEyE/hnEzQxt1zu5Kog==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+
+ '@react-email/heading@0.0.16':
+ resolution: {integrity: sha512-jmsKnQm1ykpBzw4hCYHwBkt5pW2jScXffPeEH5ZRF5tZeF5b1pvlFTO9han7C0pCkZYo1kEvWiRtx69yfCIwuw==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+
+ '@react-email/hr@0.0.12':
+ resolution: {integrity: sha512-TwmOmBDibavUQpXBxpmZYi2Iks/yeZOzFYh+di9EltMSnEabH8dMZXrl+pxNXzCgZ2XE8HY7VmUL65Lenfu5PA==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+
+ '@react-email/html@0.0.12':
+ resolution: {integrity: sha512-KTShZesan+UsreU7PDUV90afrZwU5TLwYlALuCSU0OT+/U8lULNNbAUekg+tGwCnOfIKYtpDPKkAMRdYlqUznw==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+
+ '@react-email/img@0.0.12':
+ resolution: {integrity: sha512-sRCpEARNVTf3FQhZOC+JTvu5r6ubiYWkT0ucYXg8ctkyi4G8QG+jgYPiNUqVeTLA2STOfmPM/nrk1nb84y6CPQ==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+
+ '@react-email/link@0.0.13':
+ resolution: {integrity: sha512-lkWc/NjOcefRZMkQoSDDbuKBEBDES9aXnFEOuPH845wD3TxPwh+QTf0fStuzjoRLUZWpHnio4z7qGGRYusn/sw==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+
+ '@react-email/markdown@0.0.18':
+ resolution: {integrity: sha512-gSuYK5fsMbGk87jDebqQ6fa2fKcWlkf2Dkva8kMONqLgGCq8/0d+ZQYMEJsdidIeBo3kmsnHZPrwdFB4HgjUXg==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+
+ '@react-email/preview@0.0.14':
+ resolution: {integrity: sha512-aYK8q0IPkBXyMsbpMXgxazwHxYJxTrXrV95GFuu2HbEiIToMwSyUgb8HDFYwPqqfV03/jbwqlsXmFxsOd+VNaw==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+
+ '@react-email/render@2.0.4':
+ resolution: {integrity: sha512-kht2oTFQ1SwrLpd882ahTvUtNa9s53CERHstiTbzhm6aR2Hbykp/mQ4tpPvsBGkKAEvKRlDEoooh60Uk6nHK1g==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^18.0 || ^19.0 || ^19.0.0-rc
+
+ '@react-email/row@0.0.13':
+ resolution: {integrity: sha512-bYnOac40vIKCId7IkwuLAAsa3fKfSfqCvv6epJKmPE0JBuu5qI4FHFCl9o9dVpIIS08s/ub+Y/txoMt0dYziGw==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+
+ '@react-email/section@0.0.17':
+ resolution: {integrity: sha512-qNl65ye3W0Rd5udhdORzTV9ezjb+GFqQQSae03NDzXtmJq6sqVXNWNiVolAjvJNypim+zGXmv6J9TcV5aNtE/w==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+
+ '@react-email/tailwind@2.0.5':
+ resolution: {integrity: sha512-7Ey+kiWliJdxPMCLYsdDts8ffp4idlP//w4Ui3q/A5kokVaLSNKG8DOg/8qAuzWmRiGwNQVOKBk7PXNlK5W+sg==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ '@react-email/body': 0.2.1
+ '@react-email/button': 0.2.1
+ '@react-email/code-block': 0.2.1
+ '@react-email/code-inline': 0.0.6
+ '@react-email/container': 0.0.16
+ '@react-email/heading': 0.0.16
+ '@react-email/hr': 0.0.12
+ '@react-email/img': 0.0.12
+ '@react-email/link': 0.0.13
+ '@react-email/preview': 0.0.14
+ '@react-email/text': 0.1.6
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@react-email/body':
+ optional: true
+ '@react-email/button':
+ optional: true
+ '@react-email/code-block':
+ optional: true
+ '@react-email/code-inline':
+ optional: true
+ '@react-email/container':
+ optional: true
+ '@react-email/heading':
+ optional: true
+ '@react-email/hr':
+ optional: true
+ '@react-email/img':
+ optional: true
+ '@react-email/link':
+ optional: true
+ '@react-email/preview':
+ optional: true
+
+ '@react-email/text@0.1.6':
+ resolution: {integrity: sha512-TYqkioRS45wTR5il3dYk/SbUjjEdhSwh9BtRNB99qNH1pXAwA45H7rAuxehiu8iJQJH0IyIr+6n62gBz9ezmsw==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
+ '@selderee/plugin-htmlparser2@0.11.0':
+ resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
+
'@serwist/build@9.5.6':
resolution: {integrity: sha512-/YUi2BKrvnIkYg8k/PW5N/lAR4N0h/F8eBaqCaDNOy2fdOiNCkvRaWq/ZaoYN5tocvNsMc7OSm7+m1aJqR7trQ==}
engines: {node: '>=18.0.0'}
@@ -504,6 +678,9 @@ packages:
typescript:
optional: true
+ '@stablelib/base64@1.0.1':
+ resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==}
+
'@supabase/auth-js@2.97.0':
resolution: {integrity: sha512-2Og/1lqp+AIavr8qS2X04aSl8RBY06y4LrtIAGxat06XoXYiDxKNQMQzWDAKm1EyZFZVRNH48DO5YvIZ7la5fQ==}
engines: {node: '>=20.0.0'}
@@ -1051,6 +1228,10 @@ packages:
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+ deepmerge@4.3.1:
+ resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
+ engines: {node: '>=0.10.0'}
+
define-data-property@1.1.4:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'}
@@ -1067,6 +1248,19 @@ packages:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
+ dom-serializer@2.0.0:
+ resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
+
+ domelementtype@2.3.0:
+ resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
+
+ domhandler@5.0.3:
+ resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
+ engines: {node: '>= 4'}
+
+ domutils@3.2.2:
+ resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
+
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -1087,6 +1281,10 @@ packages:
resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==}
engines: {node: '>=10.13.0'}
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
es-abstract@1.24.1:
resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==}
engines: {node: '>= 0.4'}
@@ -1260,6 +1458,9 @@ packages:
fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+ fast-sha256@1.3.0:
+ resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==}
+
fastq@1.20.1:
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
@@ -1422,6 +1623,13 @@ packages:
hermes-parser@0.25.1:
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
+ html-to-text@9.0.5:
+ resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
+ engines: {node: '>=14'}
+
+ htmlparser2@8.0.2:
+ resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
+
https-proxy-agent@7.0.6:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
@@ -1624,6 +1832,9 @@ packages:
resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==}
engines: {node: '>=0.10'}
+ leac@0.6.0:
+ resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
+
levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
@@ -1726,6 +1937,11 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+ marked@15.0.12:
+ resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==}
+ engines: {node: '>= 18'}
+ hasBin: true
+
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -1883,6 +2099,9 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
+ parseley@0.12.1:
+ resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==}
+
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -1898,6 +2117,9 @@ packages:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
+ peberminta@0.9.0:
+ resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
+
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -1913,6 +2135,9 @@ packages:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
+ postal-mime@2.7.3:
+ resolution: {integrity: sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw==}
+
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
@@ -1928,10 +2153,19 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
+ prettier@3.8.1:
+ resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==}
+ engines: {node: '>=14'}
+ hasBin: true
+
pretty-bytes@6.1.1:
resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
engines: {node: ^14.13.1 || >=16.0.0}
+ prismjs@1.30.0:
+ resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
+ engines: {node: '>=6'}
+
proc-log@5.0.0:
resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==}
engines: {node: ^18.17.0 || >=20.5.0}
@@ -1946,6 +2180,12 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+ react-colorful@5.6.1:
+ resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
react-dom@19.2.3:
resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==}
peerDependencies:
@@ -1970,6 +2210,15 @@ packages:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'}
+ resend@6.9.2:
+ resolution: {integrity: sha512-uIM6CQ08tS+hTCRuKBFbOBvHIGaEhqZe8s4FOgqsVXSbQLAhmNWpmUhG3UAtRnmcwTWFUqnHa/+Vux8YGPyDBA==}
+ engines: {node: '>=20'}
+ peerDependencies:
+ '@react-email/render': '*'
+ peerDependenciesMeta:
+ '@react-email/render':
+ optional: true
+
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -2009,6 +2258,9 @@ packages:
scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
+ selderee@0.11.0:
+ resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
+
semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
@@ -2087,6 +2339,9 @@ packages:
stable-hash@0.0.5:
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
+ standardwebhooks@1.0.0:
+ resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==}
+
stop-iteration-iterator@1.1.0:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
@@ -2164,6 +2419,9 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
+ svix@1.84.1:
+ resolution: {integrity: sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==}
+
tailwind-merge@2.6.1:
resolution: {integrity: sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==}
@@ -2252,6 +2510,10 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+ uuid@10.0.0:
+ resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
+ hasBin: true
+
web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
@@ -2691,8 +2953,137 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
+ '@react-email/body@0.2.1(react@19.2.3)':
+ dependencies:
+ react: 19.2.3
+
+ '@react-email/button@0.2.1(react@19.2.3)':
+ dependencies:
+ react: 19.2.3
+
+ '@react-email/code-block@0.2.1(react@19.2.3)':
+ dependencies:
+ prismjs: 1.30.0
+ react: 19.2.3
+
+ '@react-email/code-inline@0.0.6(react@19.2.3)':
+ dependencies:
+ react: 19.2.3
+
+ '@react-email/column@0.0.14(react@19.2.3)':
+ dependencies:
+ react: 19.2.3
+
+ '@react-email/components@1.0.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
+ dependencies:
+ '@react-email/body': 0.2.1(react@19.2.3)
+ '@react-email/button': 0.2.1(react@19.2.3)
+ '@react-email/code-block': 0.2.1(react@19.2.3)
+ '@react-email/code-inline': 0.0.6(react@19.2.3)
+ '@react-email/column': 0.0.14(react@19.2.3)
+ '@react-email/container': 0.0.16(react@19.2.3)
+ '@react-email/font': 0.0.10(react@19.2.3)
+ '@react-email/head': 0.0.13(react@19.2.3)
+ '@react-email/heading': 0.0.16(react@19.2.3)
+ '@react-email/hr': 0.0.12(react@19.2.3)
+ '@react-email/html': 0.0.12(react@19.2.3)
+ '@react-email/img': 0.0.12(react@19.2.3)
+ '@react-email/link': 0.0.13(react@19.2.3)
+ '@react-email/markdown': 0.0.18(react@19.2.3)
+ '@react-email/preview': 0.0.14(react@19.2.3)
+ '@react-email/render': 2.0.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@react-email/row': 0.0.13(react@19.2.3)
+ '@react-email/section': 0.0.17(react@19.2.3)
+ '@react-email/tailwind': 2.0.5(@react-email/body@0.2.1(react@19.2.3))(@react-email/button@0.2.1(react@19.2.3))(@react-email/code-block@0.2.1(react@19.2.3))(@react-email/code-inline@0.0.6(react@19.2.3))(@react-email/container@0.0.16(react@19.2.3))(@react-email/heading@0.0.16(react@19.2.3))(@react-email/hr@0.0.12(react@19.2.3))(@react-email/img@0.0.12(react@19.2.3))(@react-email/link@0.0.13(react@19.2.3))(@react-email/preview@0.0.14(react@19.2.3))(@react-email/text@0.1.6(react@19.2.3))(react@19.2.3)
+ '@react-email/text': 0.1.6(react@19.2.3)
+ react: 19.2.3
+ transitivePeerDependencies:
+ - react-dom
+
+ '@react-email/container@0.0.16(react@19.2.3)':
+ dependencies:
+ react: 19.2.3
+
+ '@react-email/font@0.0.10(react@19.2.3)':
+ dependencies:
+ react: 19.2.3
+
+ '@react-email/head@0.0.13(react@19.2.3)':
+ dependencies:
+ react: 19.2.3
+
+ '@react-email/heading@0.0.16(react@19.2.3)':
+ dependencies:
+ react: 19.2.3
+
+ '@react-email/hr@0.0.12(react@19.2.3)':
+ dependencies:
+ react: 19.2.3
+
+ '@react-email/html@0.0.12(react@19.2.3)':
+ dependencies:
+ react: 19.2.3
+
+ '@react-email/img@0.0.12(react@19.2.3)':
+ dependencies:
+ react: 19.2.3
+
+ '@react-email/link@0.0.13(react@19.2.3)':
+ dependencies:
+ react: 19.2.3
+
+ '@react-email/markdown@0.0.18(react@19.2.3)':
+ dependencies:
+ marked: 15.0.12
+ react: 19.2.3
+
+ '@react-email/preview@0.0.14(react@19.2.3)':
+ dependencies:
+ react: 19.2.3
+
+ '@react-email/render@2.0.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
+ dependencies:
+ html-to-text: 9.0.5
+ prettier: 3.8.1
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+
+ '@react-email/row@0.0.13(react@19.2.3)':
+ dependencies:
+ react: 19.2.3
+
+ '@react-email/section@0.0.17(react@19.2.3)':
+ dependencies:
+ react: 19.2.3
+
+ '@react-email/tailwind@2.0.5(@react-email/body@0.2.1(react@19.2.3))(@react-email/button@0.2.1(react@19.2.3))(@react-email/code-block@0.2.1(react@19.2.3))(@react-email/code-inline@0.0.6(react@19.2.3))(@react-email/container@0.0.16(react@19.2.3))(@react-email/heading@0.0.16(react@19.2.3))(@react-email/hr@0.0.12(react@19.2.3))(@react-email/img@0.0.12(react@19.2.3))(@react-email/link@0.0.13(react@19.2.3))(@react-email/preview@0.0.14(react@19.2.3))(@react-email/text@0.1.6(react@19.2.3))(react@19.2.3)':
+ dependencies:
+ '@react-email/text': 0.1.6(react@19.2.3)
+ react: 19.2.3
+ tailwindcss: 4.2.1
+ optionalDependencies:
+ '@react-email/body': 0.2.1(react@19.2.3)
+ '@react-email/button': 0.2.1(react@19.2.3)
+ '@react-email/code-block': 0.2.1(react@19.2.3)
+ '@react-email/code-inline': 0.0.6(react@19.2.3)
+ '@react-email/container': 0.0.16(react@19.2.3)
+ '@react-email/heading': 0.0.16(react@19.2.3)
+ '@react-email/hr': 0.0.12(react@19.2.3)
+ '@react-email/img': 0.0.12(react@19.2.3)
+ '@react-email/link': 0.0.13(react@19.2.3)
+ '@react-email/preview': 0.0.14(react@19.2.3)
+
+ '@react-email/text@0.1.6(react@19.2.3)':
+ dependencies:
+ react: 19.2.3
+
'@rtsao/scc@1.1.0': {}
+ '@selderee/plugin-htmlparser2@0.11.0':
+ dependencies:
+ domhandler: 5.0.3
+ selderee: 0.11.0
+
'@serwist/build@9.5.6(browserslist@4.28.1)(typescript@5.9.3)':
dependencies:
'@serwist/utils': 9.5.6(browserslist@4.28.1)
@@ -2749,6 +3140,8 @@ snapshots:
transitivePeerDependencies:
- browserslist
+ '@stablelib/base64@1.0.1': {}
+
'@supabase/auth-js@2.97.0':
dependencies:
tslib: 2.8.1
@@ -3294,6 +3687,8 @@ snapshots:
deep-is@0.1.4: {}
+ deepmerge@4.3.1: {}
+
define-data-property@1.1.4:
dependencies:
es-define-property: 1.0.1
@@ -3312,6 +3707,24 @@ snapshots:
dependencies:
esutils: 2.0.3
+ dom-serializer@2.0.0:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ entities: 4.5.0
+
+ domelementtype@2.3.0: {}
+
+ domhandler@5.0.3:
+ dependencies:
+ domelementtype: 2.3.0
+
+ domutils@3.2.2:
+ dependencies:
+ dom-serializer: 2.0.0
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -3331,6 +3744,8 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.3.0
+ entities@4.5.0: {}
+
es-abstract@1.24.1:
dependencies:
array-buffer-byte-length: 1.0.2
@@ -3655,6 +4070,8 @@ snapshots:
fast-levenshtein@2.0.6: {}
+ fast-sha256@1.3.0: {}
+
fastq@1.20.1:
dependencies:
reusify: 1.1.0
@@ -3815,6 +4232,21 @@ snapshots:
dependencies:
hermes-estree: 0.25.1
+ html-to-text@9.0.5:
+ dependencies:
+ '@selderee/plugin-htmlparser2': 0.11.0
+ deepmerge: 4.3.1
+ dom-serializer: 2.0.0
+ htmlparser2: 8.0.2
+ selderee: 0.11.0
+
+ htmlparser2@8.0.2:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ domutils: 3.2.2
+ entities: 4.5.0
+
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.4
@@ -4017,6 +4449,8 @@ snapshots:
dependencies:
language-subtag-registry: 0.3.23
+ leac@0.6.0: {}
+
levn@0.4.1:
dependencies:
prelude-ls: 1.2.1
@@ -4097,6 +4531,8 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
+ marked@15.0.12: {}
+
math-intrinsics@1.1.0: {}
merge2@1.4.1: {}
@@ -4257,6 +4693,11 @@ snapshots:
dependencies:
callsites: 3.1.0
+ parseley@0.12.1:
+ dependencies:
+ leac: 0.6.0
+ peberminta: 0.9.0
+
path-exists@4.0.0: {}
path-key@3.1.1: {}
@@ -4268,6 +4709,8 @@ snapshots:
lru-cache: 10.4.3
minipass: 7.1.3
+ peberminta@0.9.0: {}
+
picocolors@1.1.1: {}
picomatch@2.3.1: {}
@@ -4276,6 +4719,8 @@ snapshots:
possible-typed-array-names@1.1.0: {}
+ postal-mime@2.7.3: {}
+
postcss-value-parser@4.2.0: {}
postcss@8.4.31:
@@ -4292,8 +4737,12 @@ snapshots:
prelude-ls@1.2.1: {}
+ prettier@3.8.1: {}
+
pretty-bytes@6.1.1: {}
+ prismjs@1.30.0: {}
+
proc-log@5.0.0: {}
prop-types@15.8.1:
@@ -4306,6 +4755,11 @@ snapshots:
queue-microtask@1.2.3: {}
+ react-colorful@5.6.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
+ dependencies:
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+
react-dom@19.2.3(react@19.2.3):
dependencies:
react: 19.2.3
@@ -4337,6 +4791,13 @@ snapshots:
gopd: 1.2.0
set-function-name: 2.0.2
+ resend@6.9.2(@react-email/render@2.0.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3)):
+ dependencies:
+ postal-mime: 2.7.3
+ svix: 1.84.1
+ optionalDependencies:
+ '@react-email/render': 2.0.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+
resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: {}
@@ -4383,6 +4844,10 @@ snapshots:
scheduler@0.27.0: {}
+ selderee@0.11.0:
+ dependencies:
+ parseley: 0.12.1
+
semver@6.3.1: {}
semver@7.7.3: {}
@@ -4496,6 +4961,11 @@ snapshots:
stable-hash@0.0.5: {}
+ standardwebhooks@1.0.0:
+ dependencies:
+ '@stablelib/base64': 1.0.1
+ fast-sha256: 1.3.0
+
stop-iteration-iterator@1.1.0:
dependencies:
es-errors: 1.3.0
@@ -4597,6 +5067,11 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
+ svix@1.84.1:
+ dependencies:
+ standardwebhooks: 1.0.0
+ uuid: 10.0.0
+
tailwind-merge@2.6.1: {}
tailwindcss@4.2.1: {}
@@ -4731,6 +5206,8 @@ snapshots:
dependencies:
punycode: 2.3.1
+ uuid@10.0.0: {}
+
web-streams-polyfill@3.3.3: {}
webidl-conversions@4.0.2: {}
diff --git a/src/app/(app)/account/page.tsx b/src/app/(app)/account/page.tsx
index 0da1304..1aea34f 100644
--- a/src/app/(app)/account/page.tsx
+++ b/src/app/(app)/account/page.tsx
@@ -1,31 +1,31 @@
import React from "react";
-import { User, Settings } from "lucide-react";
+import { Settings } from "lucide-react";
+import { PageHeader } from "@/components/ui/PageHeader";
export default function AccountPage() {
return (
-
-
-
Sua Conta
-
Gerencie suas informações, preferências e assinatura.
-
+
+
-
-
-
-
+
+
+
+
+
-
Configurações
+
Configurações
-
- Módulo de gerenciamento e configurações da conta em construção.
-
+
+ Módulo de gerenciamento e configurações da conta em construção.
+
-
-
-
-
-
- Em desenvolvimento
+
+
+
+
+
+ Em desenvolvimento
+
diff --git a/src/app/(app)/accounts/page.tsx b/src/app/(app)/accounts/page.tsx
index e24ab5d..358ebe0 100644
--- a/src/app/(app)/accounts/page.tsx
+++ b/src/app/(app)/accounts/page.tsx
@@ -1,12 +1,20 @@
"use client";
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
import { Plus, ChevronDown, ChevronRight } from "lucide-react";
import { motion, useScroll, useTransform } from "framer-motion";
import { cn } from "@/lib/utils";
import { AccountCard } from "@/components/ui/AccountCard";
import { AccountSlideOver } from "@/components/ui/AccountSlideOver";
+import { PageHeader } from "@/components/ui/PageHeader";
import { ArrowRight, X } from "lucide-react";
+import { CreateAccountModal } from "@/components/ui/CreateAccountModal";
+import {
+ fetchAccounts,
+ createAccountAction,
+ updateAccountAction,
+ deleteAccountAction,
+} from "@/app/actions/accountActions";
// Mock Data
const MOCK_GIRO = [
@@ -142,11 +150,52 @@ export default function AccountsPage() {
const [transferIntent, setTransferIntent] = useState<{ sourceId: string; targetId: string } | null>(null);
const [transferValue, setTransferValue] = useState("");
- const totalGiro = MOCK_GIRO.reduce((acc, curr) => acc + curr.balance, 0);
- const totalCreditBills = MOCK_CREDIT.reduce((acc, curr) => acc + (curr.creditUsed || 0), 0);
+ // Create Account State
+ const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const [giroAccounts, setGiroAccounts] = useState
([]);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const [creditAccounts, setCreditAccounts] = useState([]);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const [vaultAccounts, setVaultAccounts] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isSeeding, setIsSeeding] = useState(false);
+
+ useEffect(() => {
+ let isMounted = true;
+ async function load() {
+ const data = await fetchAccounts();
+ if (data && isMounted && data.length > 0) {
+ const mapped = data.map((account) => ({
+ id: account.id,
+ name: account.name,
+ institution: account.institution || "",
+ category: account.category,
+ balance: account.balance,
+ colorHex: account.color_hex || "#8A05BE",
+ lastSyncedAt: new Date(account.updated_at),
+ members: [{ id: "u1", name: "Você", role: "owner" as const }],
+ creditUsed: account.category === "credit" ? 0 : undefined,
+ }));
+
+ setGiroAccounts(mapped.filter((a) => ["checking", "wallet"].includes(a.category)));
+ setCreditAccounts(mapped.filter((a) => ["credit"].includes(a.category)));
+ setVaultAccounts(mapped.filter((a) => ["savings", "vault"].includes(a.category)));
+ }
+ if (isMounted) setIsLoading(false);
+ }
+ load();
+ return () => {
+ isMounted = false;
+ };
+ }, []);
+
+ const totalGiro = giroAccounts.reduce((acc, curr) => acc + curr.balance, 0);
+ const totalCreditBills = creditAccounts.reduce((acc, curr) => acc + (curr.creditUsed || 0), 0);
const realLiquidity = totalGiro - totalCreditBills;
- const totalReserves = MOCK_VAULT.reduce((acc, curr) => acc + curr.balance, 0);
+ const totalReserves = vaultAccounts.reduce((acc, curr) => acc + curr.balance, 0);
const formatCurrency = (val: number) => {
return new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(val);
@@ -178,22 +227,128 @@ export default function AccountsPage() {
e.preventDefault();
setDragOverId(null);
if (draggedAccountId && draggedAccountId !== targetId) {
+ const tempAll = [...giroAccounts, ...creditAccounts, ...vaultAccounts];
+ const targetAccount = tempAll.find((a) => a.id === targetId);
+
+ if (targetAccount?.category === "credit") {
+ alert(
+ "Não é possível transferir saldo para um Cartão de Crédito. Ele deve estar vinculado a uma conta corrente.",
+ );
+ setDraggedAccountId(null);
+ return;
+ }
+
setTransferIntent({ sourceId: draggedAccountId, targetId });
}
setDraggedAccountId(null);
};
// Combine all mock data to find specific accounts for the transfer modal
- const ALL_ACCOUNTS = [...MOCK_GIRO, ...MOCK_CREDIT, ...MOCK_VAULT];
+ const ALL_ACCOUNTS = [...giroAccounts, ...creditAccounts, ...vaultAccounts];
+
+ const handleTransferValueChange = (e: React.ChangeEvent) => {
+ const value = e.target.value.replace(/\D/g, "");
+ if (!value) {
+ setTransferValue("");
+ return;
+ }
+ const numericValue = parseInt(value, 10) / 100;
+ const formatted = new Intl.NumberFormat("pt-BR", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ }).format(numericValue);
+ setTransferValue(formatted);
+ };
const executeTransfer = () => {
- console.log(`Transferring ${transferValue} from ${transferIntent?.sourceId} to ${transferIntent?.targetId}`);
+ const value = parseFloat(transferValue.replace(/\./g, "").replace(",", "."));
+ if (isNaN(value) || value <= 0 || !transferIntent) return;
+
+ const { sourceId, targetId } = transferIntent;
+
+ const updateBalance = (id: string, amount: number) => {
+ setGiroAccounts((prev) => prev.map((a) => (a.id === id ? { ...a, balance: a.balance + amount } : a)));
+ setCreditAccounts((prev) => prev.map((a) => (a.id === id ? { ...a, balance: a.balance + amount } : a)));
+ setVaultAccounts((prev) => prev.map((a) => (a.id === id ? { ...a, balance: a.balance + amount } : a)));
+ };
+
+ updateBalance(sourceId, -value);
+ updateBalance(targetId, value);
+
setTransferIntent(null);
setTransferValue("");
};
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const handleUpdateAccount = async (id: string, updates: any) => {
+ const res = await updateAccountAction(id, updates);
+ if (res.success) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const mapUpdate = (prev: any[]) =>
+ prev.map((a) => (a.id === id ? { ...a, ...updates, colorHex: updates.color_hex || a.colorHex } : a));
+ setGiroAccounts(mapUpdate);
+ setCreditAccounts(mapUpdate);
+ setVaultAccounts(mapUpdate);
+ }
+ };
+
+ const handleDeleteAccount = async (id: string) => {
+ const res = await deleteAccountAction(id);
+ if (res.success) {
+ setGiroAccounts((prev) => prev.filter((a) => a.id !== id));
+ setCreditAccounts((prev) => prev.filter((a) => a.id !== id));
+ setVaultAccounts((prev) => prev.filter((a) => a.id !== id));
+ }
+ };
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const handleCreateAccount = async (newAccount: any) => {
+ const accountInsert = {
+ name: newAccount.name,
+ institution: newAccount.institution,
+ category: newAccount.category,
+ balance: newAccount.balance || 0,
+ color_hex: newAccount.colorHex,
+ };
+ const res = await createAccountAction(accountInsert as any);
+
+ if (res.success && res.data) {
+ const dbAccount = {
+ ...newAccount,
+ id: res.data.id,
+ };
+
+ if (
+ dbAccount.category === "checking" ||
+ dbAccount.category === "savings" ||
+ dbAccount.category === "wallet"
+ ) {
+ setGiroAccounts((prev) => [...prev, dbAccount]);
+ } else if (dbAccount.category === "credit") {
+ setCreditAccounts((prev) => [...prev, dbAccount]);
+ } else if (dbAccount.category === "vault") {
+ setVaultAccounts((prev) => [...prev, dbAccount]);
+ }
+ }
+ };
+
+ const handleSeedData = async () => {
+ setIsSeeding(true);
+ const defaults = [...MOCK_GIRO, ...MOCK_CREDIT, ...MOCK_VAULT];
+ for (const acc of defaults) {
+ await createAccountAction({
+ name: acc.name,
+ institution: acc.institution,
+ category: acc.category,
+ balance: acc.balance || 0,
+ color_hex: acc.colorHex,
+ } as any);
+ }
+ window.location.reload();
+ };
+
return (
-
+
{/* Subtle Background Glow w/ Parallax */}
{/* Header Macro Context */}
-
-
-
Liquidez Imediata
-
-
- {formatCurrency(realLiquidity)}
-
-
- Livre de faturas fechadas
-
-
-
-
-
Patrimônio Alocado (Reservas):
-
{formatCurrency(totalReserves)}
+
{formatCurrency(realLiquidity)}}
+ badge="Livre de faturas fechadas"
+ action={
+
+ {!isLoading &&
+ giroAccounts.length === 0 &&
+ creditAccounts.length === 0 &&
+ vaultAccounts.length === 0 && (
+
+ )}
+
+ }
+ >
+
+
Patrimônio Alocado (Reservas):
+
{formatCurrency(totalReserves)}
-
-
-
+
{/* Accordions */}
- {MOCK_GIRO.map((account) => (
+ {giroAccounts.map((account) => (
- {MOCK_CREDIT.map((account) => (
+ {creditAccounts.map((account) => (
);
})()}
+
+ setIsCreateModalOpen(false)}
+ onCreate={handleCreateAccount}
+ />
);
}
diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx
index ad71c8b..cb83825 100644
--- a/src/app/(app)/dashboard/page.tsx
+++ b/src/app/(app)/dashboard/page.tsx
@@ -1,27 +1,32 @@
import React from "react";
import { Construction } from "lucide-react";
+import { PageHeader } from "@/components/ui/PageHeader";
export default function DashboardPage() {
return (
-
-
-
-
-
+
+
+
+
+
+
+
+
-
Dashboard
+
Dashboard
-
- Esta área está atualmente em construção. Em breve você terá acesso a todas as métricas detalhadas de
- fluxo de caixa aqui.
-
+
+ Esta área está atualmente em construção. Em breve você terá acesso a todas as métricas
+ detalhadas de fluxo de caixa aqui.
+
-
-
-
-
-
- Em desenvolvimento
+
+
+
+
+
+ Em desenvolvimento
+
diff --git a/src/app/(app)/investments/page.tsx b/src/app/(app)/investments/page.tsx
index 02c2ef9..50cbf29 100644
--- a/src/app/(app)/investments/page.tsx
+++ b/src/app/(app)/investments/page.tsx
@@ -1,27 +1,32 @@
import React from "react";
import { TrendingUp } from "lucide-react";
+import { PageHeader } from "@/components/ui/PageHeader";
export default function InvestmentsPage() {
return (
-
-
-
-
-
+
+
+
+
+
+
+
+
-
Investimentos
+
Investimentos
-
- Esta área está atualmente em construção. Em breve você terá acesso ao módulo de acompanhamento de
- investimentos aqui.
-
+
+ Esta área está atualmente em construção. Em breve você terá acesso ao módulo de acompanhamento
+ de investimentos aqui.
+
-
-
-
-
-
- Em desenvolvimento
+
+
+
+
+
+ Em desenvolvimento
+
diff --git a/src/app/(app)/invoices/page.tsx b/src/app/(app)/invoices/page.tsx
index ec322cc..8b51788 100644
--- a/src/app/(app)/invoices/page.tsx
+++ b/src/app/(app)/invoices/page.tsx
@@ -1,27 +1,32 @@
import React from "react";
import { Receipt } from "lucide-react";
+import { PageHeader } from "@/components/ui/PageHeader";
export default function InvoicesPage() {
return (
-
-
-
-
-
+
+
+
+
+
+
+
+
-
Faturas
+
Faturas
-
- Esta área está atualmente em construção. Em breve você terá acesso a todas as suas faturas de
- cartões aqui.
-
+
+ Esta área está atualmente em construção. Em breve você terá acesso a todas as suas faturas de
+ cartões aqui.
+
-
-
-
-
-
- Em desenvolvimento
+
+
+
+
+
+ Em desenvolvimento
+
diff --git a/src/app/(app)/settings/page.tsx b/src/app/(app)/settings/page.tsx
index 885c8dd..8c9060a 100644
--- a/src/app/(app)/settings/page.tsx
+++ b/src/app/(app)/settings/page.tsx
@@ -1,26 +1,32 @@
import React from "react";
import { Settings } from "lucide-react";
+import { PageHeader } from "@/components/ui/PageHeader";
export default function SettingsPage() {
return (
-
-
-
-
-
+
+
+
+
+
+
+
+
-
Configurações
+
Configurações
-
- Esta área está atualmente em construção. Em breve você terá acesso às configurações exclusivas aqui.
-
+
+ Esta área está atualmente em construção. Em breve você terá acesso às configurações exclusivas
+ aqui.
+
-
-
-
-
-
- Em desenvolvimento
+
+
+
+
+
+ Em desenvolvimento
+
diff --git a/src/app/actions/accountActions.ts b/src/app/actions/accountActions.ts
new file mode 100644
index 0000000..3ee109a
--- /dev/null
+++ b/src/app/actions/accountActions.ts
@@ -0,0 +1,69 @@
+"use server";
+
+import { createClient } from "@/lib/supabase/server";
+import { revalidatePath } from "next/cache";
+import { Database } from "@/types/supabase";
+import { randomUUID } from "crypto";
+
+type AccountInsert = Database["public"]["Tables"]["accounts"]["Insert"];
+type AccountUpdate = Database["public"]["Tables"]["accounts"]["Update"];
+
+export async function fetchAccounts() {
+ const supabase = await createClient();
+ const { data, error } = await supabase.from("accounts").select("*").order("created_at", { ascending: false });
+
+ if (error) {
+ console.error("Error fetching accounts:", error);
+ return [];
+ }
+
+ return data;
+}
+
+export async function createAccountAction(account: AccountInsert) {
+ const supabase = await createClient();
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
+
+ if (!user) {
+ return { success: false, error: "Usuário não autenticado." };
+ }
+
+ const id = account.id || randomUUID();
+ const { error } = await supabase.from("accounts").insert({ ...account, id });
+
+ if (error) {
+ console.error("Error creating account:", error);
+ return { success: false, error: error.message };
+ }
+
+ revalidatePath("/accounts");
+ return { success: true, data: { ...account, id } };
+}
+
+export async function updateAccountAction(id: string, updates: AccountUpdate) {
+ const supabase = await createClient();
+ const { error } = await supabase.from("accounts").update(updates).eq("id", id);
+
+ if (error) {
+ console.error("Error updating account:", error);
+ return { success: false, error: error.message };
+ }
+
+ revalidatePath("/accounts");
+ return { success: true };
+}
+
+export async function deleteAccountAction(id: string) {
+ const supabase = await createClient();
+ const { error } = await supabase.from("accounts").delete().eq("id", id);
+
+ if (error) {
+ console.error("Error deleting account:", error);
+ return { success: false, error: error.message };
+ }
+
+ revalidatePath("/accounts");
+ return { success: true };
+}
diff --git a/src/app/actions/sendAccountInvite.ts b/src/app/actions/sendAccountInvite.ts
new file mode 100644
index 0000000..80fb1d5
--- /dev/null
+++ b/src/app/actions/sendAccountInvite.ts
@@ -0,0 +1,40 @@
+"use server";
+
+import { Resend } from "resend";
+import { AccountInviteEmail } from "@/components/emails/AccountInviteEmail";
+
+const resend = new Resend(process.env.RESEND_API_KEY || "re_123");
+
+export async function sendAccountInvite(data: {
+ email: string;
+ inviterName: string;
+ accountName: string;
+ role: string;
+}) {
+ try {
+ const baseUrl =
+ process.env.NODE_ENV === "development" ? "http://localhost:9991" : "https://finiza.inovacode.dev";
+ const inviteLink = `${baseUrl}/invite?email=${encodeURIComponent(data.email)}&role=${encodeURIComponent(data.role)}&account=${encodeURIComponent(data.accountName)}`;
+
+ const { data: resendData, error } = await resend.emails.send({
+ from: "Finiza
", // Replace with your verified domain
+ to: [data.email],
+ subject: `Convite para participar da conta: ${data.accountName}`,
+ react: AccountInviteEmail({
+ inviterName: data.inviterName,
+ accountName: data.accountName,
+ inviteLink,
+ }) as React.ReactElement,
+ });
+
+ if (error) {
+ console.error(error);
+ return { success: false, error: error.message };
+ }
+
+ return { success: true, data: resendData };
+ } catch (err: unknown) {
+ console.error(err);
+ return { success: false, error: err instanceof Error ? err.message : "Unknown error" };
+ }
+}
diff --git a/src/app/invite/page.tsx b/src/app/invite/page.tsx
new file mode 100644
index 0000000..caf8f9b
--- /dev/null
+++ b/src/app/invite/page.tsx
@@ -0,0 +1,22 @@
+import React, { Suspense } from "react";
+import { InviteView } from "@/components/invite/InviteView";
+
+export const metadata = {
+ title: "Convite Pendente | Finiza",
+ description: "Aceite seu convite para participar de uma conta no Finiza.",
+};
+
+export default function InvitePage() {
+ return (
+
+ {/* Background igual ao da tela de Auth */}
+
+
+
+ Carregando informações do convite...
}>
+
+
+
+
+ );
+}
diff --git a/src/components/auth/OtpForm.tsx b/src/components/auth/OtpForm.tsx
index c656eca..c8042f8 100644
--- a/src/components/auth/OtpForm.tsx
+++ b/src/components/auth/OtpForm.tsx
@@ -15,6 +15,9 @@ export function OtpForm() {
const supabase = createClient();
const router = useRouter();
+ // Obtém parâmetros de busca da URL para manter o redirect
+ const searchParams = typeof window !== "undefined" ? new URLSearchParams(window.location.search) : null;
+ const redirectTo = searchParams?.get("redirect_to") || "/dashboard";
const handleSendOtp = async (e: React.FormEvent) => {
e.preventDefault();
@@ -52,7 +55,7 @@ export function OtpForm() {
if (verifyError) throw verifyError;
- router.push("/dashboard");
+ router.push(redirectTo);
router.refresh();
} catch (err) {
setError((err as Error).message || "Código inválido ou expirado.");
diff --git a/src/components/emails/AccountInviteEmail.tsx b/src/components/emails/AccountInviteEmail.tsx
new file mode 100644
index 0000000..7d12b45
--- /dev/null
+++ b/src/components/emails/AccountInviteEmail.tsx
@@ -0,0 +1,212 @@
+import {
+ Body,
+ Button,
+ Container,
+ Head,
+ Heading,
+ Hr,
+ Html,
+ Link,
+ Preview,
+ Section,
+ Text,
+} from "@react-email/components";
+import React from "react";
+
+interface AccountInviteEmailProps {
+ inviterName: string;
+ accountName: string;
+ inviteLink: string;
+}
+
+export const AccountInviteEmail: React.FC
= ({
+ inviterName = "Você",
+ accountName = "Conta Conjunta",
+ inviteLink = "https://finiza.inovacode.dev/invite/123",
+}) => {
+ const previewText = `Você foi convidado(a) por ${inviterName} para participar da conta financeira ${accountName} no Finiza.`;
+
+ return (
+
+
+
+
+ {previewText}
+
+
+
+
+
+
+ Você foi convidado!
+
+
+ Você foi convidado(a) por {inviterName}{" "}
+ para participar da conta financeira{" "}
+ {accountName} no Finiza.
+
+
+
+ Ao aceitar este convite, você terá acesso à conta através da Sincronia Doméstica da
+ plataforma.
+
+
+
+
+
+
+
+
+
+ Se você não esperava por este convite, por favor, ignore este email de forma segura. O
+ link expirará em breve.
+
+
+
+
+
+ © {new Date().getFullYear()} Finiza. Todos os direitos reservados.
+
+
+ Se precisar de ajuda, entre em contato através do nosso{" "}
+
+ suporte
+
+ .
+
+
+
+
+
+
+ );
+};
+
+const main = {
+ backgroundColor: "#09090b",
+ backgroundImage: "radial-gradient(circle at 50% -20%, rgba(120, 119, 198, 0.15), transparent)",
+ color: "#fafafa",
+ margin: "0",
+ padding: "0",
+ fontFamily: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
+};
+
+const containerWrapper = {
+ padding: "40px 20px",
+};
+
+const container = {
+ maxWidth: "600px",
+ margin: "0 auto",
+ backgroundColor: "#18181b",
+ background: "rgba(24, 24, 27, 0.6)",
+ borderRadius: "16px",
+ border: "1px solid rgba(255, 255, 255, 0.1)",
+ boxShadow: "0 8px 32px 0 rgba(0, 0, 0, 0.3)",
+ overflow: "hidden",
+};
+
+const header = {
+ padding: "32px 24px",
+ textAlign: "center" as const,
+ borderBottom: "1px solid rgba(255, 255, 255, 0.05)",
+};
+
+const logo = {
+ color: "#fafafa",
+ fontSize: "28px",
+ fontWeight: "700",
+ letterSpacing: "-1px",
+ textDecoration: "none",
+ margin: "0",
+};
+
+const logoAccent = {
+ color: "#34d399",
+};
+
+const content = {
+ padding: "40px 32px",
+};
+
+const title = {
+ fontSize: "22px",
+ fontWeight: "600",
+ color: "#fafafa",
+ marginTop: "0",
+ marginBottom: "16px",
+ textAlign: "center" as const,
+};
+
+const text = {
+ color: "#a1a1aa",
+ fontSize: "16px",
+ textAlign: "center" as const,
+ lineHeight: "1.6",
+ margin: "0 0 24px 0",
+};
+
+const buttonContainer = {
+ textAlign: "center" as const,
+ marginBottom: "32px",
+ marginTop: "32px",
+};
+
+const button = {
+ backgroundColor: "#10b981",
+ backgroundImage: "linear-gradient(to right, #34d399, #10b981)",
+ color: "#09090b",
+ fontWeight: "600",
+ textDecoration: "none",
+ padding: "14px 28px",
+ borderRadius: "8px",
+ fontSize: "16px",
+ boxShadow: "0 4px 14px 0 rgba(16, 185, 129, 0.39)",
+ display: "inline-block",
+};
+
+const divider = {
+ height: "1px",
+ backgroundColor: "rgba(255, 255, 255, 0.1)",
+ margin: "32px 0 24px 0",
+ border: "none",
+ width: "100%",
+};
+
+const footer = {
+ padding: "0 32px 32px",
+ textAlign: "center" as const,
+};
+
+const footerText = {
+ margin: "0 0 8px 0",
+ fontSize: "13px",
+ color: "#71717a",
+};
+
+const link = {
+ color: "#34d399",
+ textDecoration: "none",
+};
+
+export default AccountInviteEmail;
diff --git a/src/components/invite/InviteView.tsx b/src/components/invite/InviteView.tsx
new file mode 100644
index 0000000..1656b13
--- /dev/null
+++ b/src/components/invite/InviteView.tsx
@@ -0,0 +1,147 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { createClient } from "@/lib/supabase/client";
+import { useRouter, useSearchParams } from "next/navigation";
+import { GlassCard } from "@/components/ui/GlassCard";
+import { Loader2, UserPlus, CheckCircle2, ArrowRight } from "lucide-react";
+
+export function InviteView() {
+ const searchParams = useSearchParams();
+ const router = useRouter();
+ const supabase = createClient();
+
+ const email = searchParams?.get("email") || "";
+ const role = searchParams?.get("role") || "Leitor";
+ const account = searchParams?.get("account") || "Indefinida";
+
+ const [loading, setLoading] = useState(true);
+ const [userEmail, setUserEmail] = useState(null);
+ const [accepted, setAccepted] = useState(false);
+
+ useEffect(() => {
+ let isMounted = true;
+
+ const checkAuth = async () => {
+ const {
+ data: { session },
+ } = await supabase.auth.getSession();
+
+ if (!isMounted) return;
+
+ if (!session) {
+ // Usuário não autenticado: enviamos para /auth com a query redirect_to apontando de volta pra cá
+ const currentFullParams = searchParams ? searchParams.toString() : "";
+ const inviteUrl = `/invite?${currentFullParams}`;
+ router.push(`/auth?redirect_to=${encodeURIComponent(inviteUrl)}`);
+ return;
+ }
+
+ // Autenticado: continua a renderizar o convite
+ setUserEmail(session.user.email ?? null);
+ setLoading(false);
+ };
+
+ checkAuth();
+
+ return () => {
+ isMounted = false;
+ };
+ }, [router, supabase, searchParams]);
+
+ const handleAcceptInvite = async () => {
+ setLoading(true);
+ // FIXME: Aqui entraria a chamada de API real do Supabase
+ // para inserir o registro na tabela de relacionamentos (account_users)
+ await new Promise((resolve) => setTimeout(resolve, 1500));
+ setAccepted(true);
+ setLoading(false);
+ };
+
+ if (loading) {
+ return (
+
+
+ Verificando informações...
+
+ );
+ }
+
+ if (accepted) {
+ return (
+
+
+
+
+
+
Convite Aceito!
+
+ Agora você tem acesso à conta {account} com permissão
+ de {role}.
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
Convite para Conta
+
+ Você foi convidado(a) para acessar a Sincronia Doméstica desta conta no Finiza.
+
+
+
+
+
+ Conta Base
+
+ {account}
+
+
+
+ Sua Permissão
+ {role}
+
+
+
+ {email && email !== userEmail && (
+
+ Aviso: O convite visava {email}, mas você está conectando como {userEmail}. Ao
+ prosseguir, o vínculo será feito ao perfil atual.
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/ui/AccountSlideOver.tsx b/src/components/ui/AccountSlideOver.tsx
index e50acc0..a76efe1 100644
--- a/src/components/ui/AccountSlideOver.tsx
+++ b/src/components/ui/AccountSlideOver.tsx
@@ -1,15 +1,20 @@
import React, { useState, useEffect } from "react";
-import { X, Check, Trash2, Edit2, Zap } from "lucide-react";
+import { X, Check, Trash2, Edit2, Zap, Plus, Loader2, ChevronDown } from "lucide-react";
+import { HexColorPicker } from "react-colorful";
import { cn } from "@/lib/utils";
+import { sendAccountInvite } from "@/app/actions/sendAccountInvite";
interface AccountSlideOverProps {
isOpen: boolean;
onClose: () => void;
- accountId: string;
+ accountId?: string;
name: string;
institution: string;
balance: number;
colorHex: string;
+ category?: string;
+ onUpdate?: (id: string, updates: Record) => void;
+ onDelete?: (id: string) => void;
}
export function AccountSlideOver({
@@ -20,11 +25,63 @@ export function AccountSlideOver({
institution,
balance,
colorHex,
+ category = "checking",
+ onUpdate,
+ onDelete,
}: AccountSlideOverProps) {
+ const [tempCategory, setTempCategory] = useState(category);
+
+ useEffect(() => {
+ setTempCategory(category);
+ }, [category]);
+
const [activeTab, setActiveTab] = useState<"ajuste" | "historico" | "config">("ajuste");
- const [adjustedBalance, setAdjustedBalance] = useState(balance.toString());
+ const [adjustedBalance, setAdjustedBalance] = useState(() =>
+ new Intl.NumberFormat("pt-BR", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ }).format(balance),
+ );
+
+ const handleBalanceChange = (e: React.ChangeEvent) => {
+ let value = e.target.value.replace(/\D/g, "");
+ if (value === "") value = "0";
+ const numericValue = parseInt(value, 10) / 100;
+ const formatted = new Intl.NumberFormat("pt-BR", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ }).format(numericValue);
+ setAdjustedBalance(formatted);
+ };
const [swipeLeftId, setSwipeLeftId] = useState(null);
+ const [isEditingInstitution, setIsEditingInstitution] = useState(false);
+ const [tempInstitution, setTempInstitution] = useState(institution);
+
+ const [isAddingPerson, setIsAddingPerson] = useState(false);
+ const [isInviting, setIsInviting] = useState(false);
+ const [newPersonEmail, setNewPersonEmail] = useState("");
+ const [addedPersons, setAddedPersons] = useState<{ email: string; role: string }[]>([]);
+
+ const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
+ const [actualColor, setActualColor] = useState(colorHex);
+
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+
+ const handleInstitutionSave = () => {
+ setIsEditingInstitution(false);
+ if (accountId && tempInstitution !== institution && onUpdate) {
+ onUpdate(accountId, { institution: tempInstitution });
+ }
+ };
+
+ const handleColorSave = (newColor: string) => {
+ setActualColor(newColor);
+ if (accountId && newColor !== colorHex && onUpdate) {
+ onUpdate(accountId, { color_hex: newColor });
+ }
+ };
+
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
@@ -50,6 +107,46 @@ export function AccountSlideOver({
onClose();
};
+ const handleInvitePerson = async () => {
+ if (!newPersonEmail) return;
+
+ setIsInviting(true);
+ try {
+ // Fake API call or connect to server action
+ const res = await sendAccountInvite({
+ email: newPersonEmail,
+ inviterName: "Você", // Default mock as the current auth user
+ accountName: name,
+ role: "Leitor",
+ });
+
+ if (res.success) {
+ // If ok, add dynamically
+ setAddedPersons((prev: { email: string; role: string }[]) => [
+ ...prev,
+ { email: newPersonEmail, role: "Leitor" },
+ ]);
+ setNewPersonEmail("");
+ setIsAddingPerson(false);
+ } else {
+ console.error("Failed to send invite:", res.error);
+ alert("Falha ao enviar convite. " + res.error);
+ }
+ } catch (error) {
+ console.error("Unexpected error:", error);
+ alert("Erro inesperado ao enviar convite.");
+ setIsInviting(false);
+ }
+ };
+
+ const handleDeleteAccount = () => {
+ if (accountId && onDelete) {
+ onDelete(accountId);
+ }
+ setIsDeleteModalOpen(false);
+ onClose();
+ };
+
return (
<>
{/* Backdrop */}
@@ -69,15 +166,15 @@ export function AccountSlideOver({
>
{/* Glow behind the sidebar */}
{/* Header */}
{name}
-
{institution}
+
{tempInstitution}