diff --git a/package.json b/package.json index 91ebacc8..06b6e5a8 100644 --- a/package.json +++ b/package.json @@ -56,11 +56,13 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", + "@react-email/components": "^1.0.12", "@socket.io/redis-adapter": "^8.3.0", "@tanstack/query-broadcast-client-experimental": "^5.100.10", "@tanstack/react-query": "^5.100.10", "@tanstack/react-query-devtools": "^5.100.10", "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.24", "@types/pdfkit": "^0.17.6", "archiver": "^7.0.1", "better-auth": "^1.6.10", @@ -92,6 +94,7 @@ "react-day-picker": "^10.0.0", "react-dom": "^19.2.6", "react-easy-crop": "^5.5.7", + "react-email": "^6.1.3", "react-hook-form": "^7.75.0", "recharts": "^3.8.1", "sharp": "^0.34.5", @@ -120,6 +123,7 @@ "@types/nodemailer": "^8.0.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", "@vitest/coverage-v8": "^4.1.6", "autoprefixer": "^10.5.0", "dotenv": "^17.4.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5806093..146ae6ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,6 +97,9 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.2.8 version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-email/components': + specifier: ^1.0.12 + version: 1.0.12(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@socket.io/redis-adapter': specifier: ^8.3.0 version: 8.3.0(socket.io-adapter@2.5.6) @@ -112,6 +115,9 @@ importers: '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/react-virtual': + specifier: ^3.13.24 + version: 3.13.24(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@types/pdfkit': specifier: ^0.17.6 version: 0.17.6 @@ -205,6 +211,9 @@ importers: react-easy-crop: specifier: ^5.5.7 version: 5.5.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-email: + specifier: ^6.1.3 + version: 6.1.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react-hook-form: specifier: ^7.75.0 version: 7.75.0(react@19.2.6) @@ -284,6 +293,9 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@20.19.41)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.4)) '@vitest/coverage-v8': specifier: ^4.1.6 version: 4.1.6(vitest@4.1.6) @@ -369,6 +381,11 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/parser@7.27.0': + resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/parser@7.29.3': resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} engines: {node: '>=6.0.0'} @@ -386,6 +403,10 @@ packages: resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.27.0': + resolution: {integrity: sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==} + engines: {node: '>=6.9.0'} + '@babel/traverse@7.29.0': resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} engines: {node: '>=6.9.0'} @@ -1752,6 +1773,192 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@react-email/body@0.3.0': + resolution: {integrity: sha512-uGo0BOOzjbMUo3lu+BIDWayvn5o6Xyfmnlla5VGf05n8gHMvO1ll7U4FtzWe3hxMLwt53pmc4iE0M+B5slG+Ug==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + 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'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + 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'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + 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'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + 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'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/components@1.0.12': + resolution: {integrity: sha512-tH18JhPDWgE+3jnYkzyB6ZrZdfNnEsFe4PwmuXmlOw4NGIysP8wPY5aXZg++pTG9qUabXg1nzX/FGHGkObH8xQ==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + 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'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + 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'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + 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'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + 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'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + 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'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + 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'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + 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'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + 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'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + 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'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + 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'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/render@2.0.6': + resolution: {integrity: sha512-xOzaYkH3jLZKqN5MqrTXYnmqBYUnZSVbkxdb5PGGmDcK6sKDVMliaDiSwfXajRC9JtSHTcGc2tmGLHWuCgVpog==} + 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/render@2.0.8': + resolution: {integrity: sha512-5udvVr3U/WuGJZfLdLBOhkzrqRWd2Q5ZYmF7ppcy7FzWcwgshdqLMNqJOXcVzAXJXg/2bm7D+WGJzTtZOZMQnQ==} + 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'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + 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'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/tailwind@2.0.7': + resolution: {integrity: sha512-kGw80weVFXikcnCXbigTGXGWQ0MRCSYNCudcdkWxebkWYd0FG6/NPoN3V1p/u68/4+NxZwYPVi2fhnp0x23HdA==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + '@react-email/body': '>=0' + '@react-email/button': '>=0' + '@react-email/code-block': '>=0' + '@react-email/code-inline': '>=0' + '@react-email/container': '>=0' + '@react-email/heading': '>=0' + '@react-email/hr': '>=0' + '@react-email/img': '>=0' + '@react-email/link': '>=0' + '@react-email/preview': '>=0' + '@react-email/text': '>=0' + 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'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-grab/cli@0.1.34': resolution: {integrity: sha512-L2eAxN46Vq2Ss3nDegrH7wQVMeWH03ahawp+OdzUtQWqL3cq6Bt149q9XhY3cWc9fJsxuWjLfCn+3T9uApIlBA==} hasBin: true @@ -1865,6 +2072,9 @@ packages: '@rolldown/pluginutils@1.0.0-rc.12': resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -1922,10 +2132,19 @@ packages: react: '>=16.8' react-dom: '>=16.8' + '@tanstack/react-virtual@3.13.24': + resolution: {integrity: sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/table-core@8.21.3': resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} + '@tanstack/virtual-core@3.14.0': + resolution: {integrity: sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==} + '@total-typescript/ts-reset@0.6.1': resolution: {integrity: sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg==} @@ -2189,6 +2408,19 @@ packages: cpu: [x64] os: [win32] + '@vitejs/plugin-react@6.0.1': + resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: 8.0.5 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + '@vitest/coverage-v8@4.1.6': resolution: {integrity: sha512-36l628fQ/9a/8ihy97eOtEnvWQEdqULQOJtcaxtoNq0G1w3Mxd4szSahOaMM9/NGyZ+hyKcMtIW/WIxq0XQViQ==} peerDependencies: @@ -2258,9 +2490,20 @@ packages: air-datepicker@3.6.0: resolution: {integrity: sha512-+txUkqa949rXBJDmkQAIb/GehZECJYF4rm9XJxVYtEX22C9WvBpE/XwCUQZBopKIkpg4ycAySJ9lH3JOg9qQTw==} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + ansi-escapes@7.3.0: resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} @@ -2363,6 +2606,9 @@ packages: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} + atomically@2.1.1: + resolution: {integrity: sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==} + autoprefixer@10.5.0: resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} engines: {node: ^10 || ^12 || >=14} @@ -2635,6 +2881,13 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + citty@0.2.2: + resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -2697,6 +2950,10 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -2716,6 +2973,10 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + conf@15.1.0: + resolution: {integrity: sha512-Uy5YN9KEu0WWDaZAVJ5FAmZoaJt9rdK6kH+utItPyGsCqCgaTKkrmZx3zoE0/3q6S3bcp3Ihkk+ZqPxWxFK5og==} + engines: {node: '>=20'} + convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} @@ -2754,6 +3015,10 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -2827,9 +3092,17 @@ packages: dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + debounce-fn@6.0.0: + resolution: {integrity: sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==} + engines: {node: '>=18'} + debounce@1.2.1: resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + debounce@2.2.0: + resolution: {integrity: sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==} + engines: {node: '>=18'} + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -2925,6 +3198,10 @@ packages: domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dot-prop@10.1.0: + resolution: {integrity: sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==} + engines: {node: '>=20'} + dotenv@17.4.2: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} @@ -3313,6 +3590,9 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fast-xml-builder@1.2.0: resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} @@ -3437,6 +3717,14 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -3749,6 +4037,10 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + jose@6.2.3: resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} @@ -3783,6 +4075,12 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -3790,6 +4088,11 @@ packages: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} @@ -3973,6 +4276,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.3.6: + resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==} + engines: {node: 20 || >=22} + lucide-react@1.14.0: resolution: {integrity: sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==} peerDependencies: @@ -3998,10 +4305,18 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + 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'} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + memory-pager@1.5.0: resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} @@ -4017,10 +4332,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -4191,6 +4514,11 @@ packages: notepack.io@3.0.1: resolution: {integrity: sha512-TKC/8zH5pXIAMVQio2TvVDTtPRX+DJPHDqjRbxogtFiByHyzKmy96RA0JtCQJ+WouyyL4A10xomQzgbUT+1jCg==} + nypm@0.6.6: + resolution: {integrity: sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==} + engines: {node: '>=18'} + hasBin: true + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -4346,6 +4674,10 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -4373,6 +4705,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + picospinner@3.0.0: + resolution: {integrity: sha512-lGA1TNsmy2bxvRsTI2cV01kfTwKzZjnZSDmF9llYNyMHMrU4sP87lQ5taiIKm88L3cbswjl008nwyGc3WpNvzg==} + engines: {node: '>=18.0.0'} + pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -4472,6 +4808,10 @@ packages: engines: {node: '>=14'} hasBin: true + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -4527,6 +4867,14 @@ packages: react: '>=16.4.0' react-dom: '>=16.4.0' + react-email@6.1.3: + resolution: {integrity: sha512-S+zukMTZGGH9xc8lPynXmPN5plRZ8MdVkTNfqkgcavAyPeUXB5nuHhCGpJAcdt9hdtsN7vjvzqgQvXT9SkEE8A==} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^18.0 || ^19.0 || ^19.0.0-rc + react-grab@0.1.34: resolution: {integrity: sha512-jtdOdv0kb90oqL+pMszSh9DOLgVRaX4ZE6XN4GkDEpNNUqveQfZT014+EeJraljpqQfuWKW+96NrrRqUD93D2g==} hasBin: true @@ -4620,6 +4968,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} @@ -4659,6 +5011,10 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} @@ -4995,6 +5351,12 @@ packages: strnum@2.3.0: resolution: {integrity: sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==} + stubborn-fs@2.0.0: + resolution: {integrity: sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==} + + stubborn-utils@1.0.2: + resolution: {integrity: sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==} + styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -5024,6 +5386,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + tailwind-merge@3.6.0: resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} @@ -5037,6 +5403,9 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + tailwindcss@4.3.0: + resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + tar-stream@3.2.0: resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} @@ -5121,6 +5490,10 @@ packages: tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} @@ -5136,6 +5509,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@5.6.0: + resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} + engines: {node: '>=20'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -5164,6 +5541,10 @@ packages: resolution: {integrity: sha512-+I6aJUv63YAcY9n4mQreLUt0d4lvwkkopDNmpomkAUz0fAkEMV9pRWxN0EjhW1YfRhcuyHg2v3mwddCDW1+LFQ==} engines: {node: '>= 4.0.0'} + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -5353,6 +5734,9 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + when-exit@2.1.5: + resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -5527,6 +5911,10 @@ snapshots: '@babel/helper-validator-identifier@7.28.5': {} + '@babel/parser@7.27.0': + dependencies: + '@babel/types': 7.29.0 + '@babel/parser@7.29.3': dependencies: '@babel/types': 7.29.0 @@ -5541,6 +5929,18 @@ snapshots: '@babel/parser': 7.29.3 '@babel/types': 7.29.0 + '@babel/traverse@7.27.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + '@babel/traverse@7.29.0': dependencies: '@babel/code-frame': 7.29.0 @@ -6809,6 +7209,137 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@react-email/body@0.3.0(react@19.2.6)': + dependencies: + react: 19.2.6 + + '@react-email/button@0.2.1(react@19.2.6)': + dependencies: + react: 19.2.6 + + '@react-email/code-block@0.2.1(react@19.2.6)': + dependencies: + prismjs: 1.30.0 + react: 19.2.6 + + '@react-email/code-inline@0.0.6(react@19.2.6)': + dependencies: + react: 19.2.6 + + '@react-email/column@0.0.14(react@19.2.6)': + dependencies: + react: 19.2.6 + + '@react-email/components@1.0.12(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@react-email/body': 0.3.0(react@19.2.6) + '@react-email/button': 0.2.1(react@19.2.6) + '@react-email/code-block': 0.2.1(react@19.2.6) + '@react-email/code-inline': 0.0.6(react@19.2.6) + '@react-email/column': 0.0.14(react@19.2.6) + '@react-email/container': 0.0.16(react@19.2.6) + '@react-email/font': 0.0.10(react@19.2.6) + '@react-email/head': 0.0.13(react@19.2.6) + '@react-email/heading': 0.0.16(react@19.2.6) + '@react-email/hr': 0.0.12(react@19.2.6) + '@react-email/html': 0.0.12(react@19.2.6) + '@react-email/img': 0.0.12(react@19.2.6) + '@react-email/link': 0.0.13(react@19.2.6) + '@react-email/markdown': 0.0.18(react@19.2.6) + '@react-email/preview': 0.0.14(react@19.2.6) + '@react-email/render': 2.0.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-email/row': 0.0.13(react@19.2.6) + '@react-email/section': 0.0.17(react@19.2.6) + '@react-email/tailwind': 2.0.7(@react-email/body@0.3.0(react@19.2.6))(@react-email/button@0.2.1(react@19.2.6))(@react-email/code-block@0.2.1(react@19.2.6))(@react-email/code-inline@0.0.6(react@19.2.6))(@react-email/container@0.0.16(react@19.2.6))(@react-email/heading@0.0.16(react@19.2.6))(@react-email/hr@0.0.12(react@19.2.6))(@react-email/img@0.0.12(react@19.2.6))(@react-email/link@0.0.13(react@19.2.6))(@react-email/preview@0.0.14(react@19.2.6))(@react-email/text@0.1.6(react@19.2.6))(react@19.2.6) + '@react-email/text': 0.1.6(react@19.2.6) + react: 19.2.6 + transitivePeerDependencies: + - react-dom + + '@react-email/container@0.0.16(react@19.2.6)': + dependencies: + react: 19.2.6 + + '@react-email/font@0.0.10(react@19.2.6)': + dependencies: + react: 19.2.6 + + '@react-email/head@0.0.13(react@19.2.6)': + dependencies: + react: 19.2.6 + + '@react-email/heading@0.0.16(react@19.2.6)': + dependencies: + react: 19.2.6 + + '@react-email/hr@0.0.12(react@19.2.6)': + dependencies: + react: 19.2.6 + + '@react-email/html@0.0.12(react@19.2.6)': + dependencies: + react: 19.2.6 + + '@react-email/img@0.0.12(react@19.2.6)': + dependencies: + react: 19.2.6 + + '@react-email/link@0.0.13(react@19.2.6)': + dependencies: + react: 19.2.6 + + '@react-email/markdown@0.0.18(react@19.2.6)': + dependencies: + marked: 15.0.12 + react: 19.2.6 + + '@react-email/preview@0.0.14(react@19.2.6)': + dependencies: + react: 19.2.6 + + '@react-email/render@2.0.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + html-to-text: 9.0.5 + prettier: 3.8.3 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + '@react-email/render@2.0.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + html-to-text: 9.0.5 + prettier: 3.8.3 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + '@react-email/row@0.0.13(react@19.2.6)': + dependencies: + react: 19.2.6 + + '@react-email/section@0.0.17(react@19.2.6)': + dependencies: + react: 19.2.6 + + '@react-email/tailwind@2.0.7(@react-email/body@0.3.0(react@19.2.6))(@react-email/button@0.2.1(react@19.2.6))(@react-email/code-block@0.2.1(react@19.2.6))(@react-email/code-inline@0.0.6(react@19.2.6))(@react-email/container@0.0.16(react@19.2.6))(@react-email/heading@0.0.16(react@19.2.6))(@react-email/hr@0.0.12(react@19.2.6))(@react-email/img@0.0.12(react@19.2.6))(@react-email/link@0.0.13(react@19.2.6))(@react-email/preview@0.0.14(react@19.2.6))(@react-email/text@0.1.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@react-email/text': 0.1.6(react@19.2.6) + react: 19.2.6 + tailwindcss: 4.3.0 + optionalDependencies: + '@react-email/body': 0.3.0(react@19.2.6) + '@react-email/button': 0.2.1(react@19.2.6) + '@react-email/code-block': 0.2.1(react@19.2.6) + '@react-email/code-inline': 0.0.6(react@19.2.6) + '@react-email/container': 0.0.16(react@19.2.6) + '@react-email/heading': 0.0.16(react@19.2.6) + '@react-email/hr': 0.0.12(react@19.2.6) + '@react-email/img': 0.0.12(react@19.2.6) + '@react-email/link': 0.0.13(react@19.2.6) + '@react-email/preview': 0.0.14(react@19.2.6) + + '@react-email/text@0.1.6(react@19.2.6)': + dependencies: + react: 19.2.6 + '@react-grab/cli@0.1.34': dependencies: commander: 14.0.3 @@ -6885,6 +7416,8 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.12': {} + '@rolldown/pluginutils@1.0.0-rc.7': {} + '@rtsao/scc@1.1.0': {} '@rushstack/eslint-patch@1.16.1': {} @@ -6943,8 +7476,16 @@ snapshots: react: 19.2.6 react-dom: 19.2.6(react@19.2.6) + '@tanstack/react-virtual@3.13.24(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@tanstack/virtual-core': 3.14.0 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + '@tanstack/table-core@8.21.3': {} + '@tanstack/virtual-core@3.14.0': {} + '@total-typescript/ts-reset@0.6.1': {} '@tybys/wasm-util@0.10.2': @@ -7199,6 +7740,11 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@vitejs/plugin-react@6.0.1(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@20.19.41)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.4))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@20.19.41)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.4) + '@vitest/coverage-v8@4.1.6(vitest@4.1.6)': dependencies: '@bcoe/v8-coverage': 1.0.2 @@ -7287,6 +7833,10 @@ snapshots: air-datepicker@3.6.0: {} + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 @@ -7294,6 +7844,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-escapes@7.3.0: dependencies: environment: 1.1.0 @@ -7432,6 +7989,11 @@ snapshots: atomic-sleep@1.0.0: {} + atomically@2.1.1: + dependencies: + stubborn-fs: 2.0.0 + when-exit: 2.1.5 + autoprefixer@10.5.0(postcss@8.5.14): dependencies: browserslist: 4.28.2 @@ -7664,6 +8226,12 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + citty@0.2.2: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -7722,6 +8290,8 @@ snapshots: colorette@2.0.20: {} + commander@13.1.0: {} + commander@14.0.3: {} commander@4.1.1: {} @@ -7738,6 +8308,18 @@ snapshots: concat-map@0.0.1: {} + conf@15.1.0: + dependencies: + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + atomically: 2.1.1 + debounce-fn: 6.0.0 + dot-prop: 10.1.0 + env-paths: 3.0.0 + json-schema-typed: 8.0.2 + semver: 7.8.0 + uint8array-extras: 1.5.0 + convert-source-map@1.9.0: {} convert-source-map@2.0.0: {} @@ -7776,6 +8358,11 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + cssesc@3.0.0: {} csstype@3.2.3: {} @@ -7842,8 +8429,14 @@ snapshots: dateformat@4.6.3: {} + debounce-fn@6.0.0: + dependencies: + mimic-function: 5.0.1 + debounce@1.2.1: {} + debounce@2.2.0: {} + debug@3.2.7: dependencies: ms: 2.1.3 @@ -7918,6 +8511,10 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + dot-prop@10.1.0: + dependencies: + type-fest: 5.6.0 + dotenv@17.4.2: {} drizzle-kit@0.31.10: @@ -7995,8 +8592,7 @@ snapshots: entities@4.5.0: {} - env-paths@3.0.0: - optional: true + env-paths@3.0.0: {} environment@1.1.0: {} @@ -8392,6 +8988,8 @@ snapshots: fast-safe-stringify@2.1.1: {} + fast-uri@3.1.2: {} + fast-xml-builder@1.2.0: dependencies: path-expression-matcher: 1.5.0 @@ -8541,6 +9139,14 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + + globals@11.12.0: {} + globals@14.0.0: {} globalthis@1.0.4: @@ -8849,6 +9455,8 @@ snapshots: jiti@1.21.7: {} + jiti@2.4.2: {} + jose@6.2.3: {} joycon@3.1.1: {} @@ -8871,12 +9479,18 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@1.0.2: dependencies: minimist: 1.2.8 + json5@2.2.3: {} + jsonc-parser@3.3.1: {} jsx-ast-utils@3.3.5: @@ -9045,6 +9659,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.3.6: {} + lucide-react@1.14.0(react@19.2.6): dependencies: react: 19.2.6 @@ -9080,8 +9696,12 @@ snapshots: dependencies: semver: 7.8.0 + marked@15.0.12: {} + math-intrinsics@1.1.0: {} + mdn-data@2.27.1: {} + memory-pager@1.5.0: optional: true @@ -9094,10 +9714,16 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mimic-function@5.0.1: {} minimatch@10.2.5: @@ -9250,6 +9876,12 @@ snapshots: notepack.io@3.0.1: {} + nypm@0.6.6: + dependencies: + citty: 0.2.2 + pathe: 2.0.3 + tinyexec: 1.1.2 + object-assign@4.1.1: {} object-hash@3.0.0: {} @@ -9405,6 +10037,11 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.3 + path-scurry@2.0.2: + dependencies: + lru-cache: 11.3.6 + minipass: 7.1.3 + path-type@4.0.0: {} pathe@2.0.3: {} @@ -9433,6 +10070,8 @@ snapshots: picomatch@4.0.4: {} + picospinner@3.0.0: {} + pify@2.3.0: {} pino-abstract-transport@3.0.0: @@ -9532,6 +10171,8 @@ snapshots: prettier@3.8.3: {} + prismjs@1.30.0: {} + process-nextick-args@2.0.1: {} process-warning@5.0.0: {} @@ -9587,6 +10228,37 @@ snapshots: react-dom: 19.2.6(react@19.2.6) tslib: 2.8.1 + react-email@6.1.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + '@babel/parser': 7.27.0 + '@babel/traverse': 7.27.0 + '@react-email/render': 2.0.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + chokidar: 4.0.3 + commander: 13.1.0 + conf: 15.1.0 + css-tree: 3.2.1 + debounce: 2.2.0 + esbuild: 0.28.0 + glob: 13.0.6 + jiti: 2.4.2 + log-symbols: 7.0.1 + marked: 15.0.12 + mime-types: 3.0.2 + normalize-path: 3.0.0 + nypm: 0.6.6 + picospinner: 3.0.0 + prismjs: 1.30.0 + prompts: 2.4.2 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + socket.io: 4.8.3 + tailwindcss: 4.3.0 + tsconfig-paths: 4.2.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + react-grab@0.1.34(react@19.2.6): dependencies: '@react-grab/cli': 0.1.34 @@ -9680,6 +10352,8 @@ snapshots: dependencies: picomatch: 2.3.2 + readdirp@4.1.2: {} + real-require@0.2.0: {} recharts@3.8.1(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react-is@18.3.1)(react@19.2.6)(redux@5.0.1): @@ -9736,6 +10410,8 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + require-from-string@2.0.2: {} + reselect@5.1.1: {} resolve-from@4.0.0: {} @@ -10168,6 +10844,12 @@ snapshots: strnum@2.3.0: {} + stubborn-fs@2.0.0: + dependencies: + stubborn-utils: 1.0.2 + + stubborn-utils@1.0.2: {} + styled-jsx@5.1.6(react@19.2.6): dependencies: client-only: 0.0.1 @@ -10191,6 +10873,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + tagged-tag@1.0.0: {} + tailwind-merge@3.6.0: {} tailwindcss-animate@1.0.7(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.4)): @@ -10225,6 +10909,8 @@ snapshots: - tsx - yaml + tailwindcss@4.3.0: {} + tar-stream@3.2.0: dependencies: b4a: 1.8.1 @@ -10326,6 +11012,12 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + tslib@1.14.1: {} tslib@2.8.1: {} @@ -10341,6 +11033,10 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@5.6.0: + dependencies: + tagged-tag: 1.0.0 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -10380,6 +11076,8 @@ snapshots: uid2@1.0.0: {} + uint8array-extras@1.5.0: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -10577,6 +11275,8 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + when-exit@2.1.5: {} + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 diff --git a/src/lib/email/templates/portal-auth.ts b/src/lib/email/templates/portal-auth.ts deleted file mode 100644 index 6868afda..00000000 --- a/src/lib/email/templates/portal-auth.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { brandingPrimaryColor, renderShell, safeUrl, type BrandingShell } from '@/lib/email/shell'; - -interface ActivationData { - portName: string; - link: string; - ttlHours: number; - recipientName?: string; -} - -interface ResetData { - portName: string; - link: string; - ttlMinutes: number; - recipientName?: string; -} - -interface RenderOpts { - subject?: string | null; - branding?: BrandingShell | null; -} - -export function activationEmail( - data: ActivationData, - overrides?: RenderOpts, -): { - subject: string; - html: string; - text: string; -} { - const subject = overrides?.subject - ? overrides.subject - .replace(/\{\{portName\}\}/g, data.portName) - .replace(/\{\{recipientName\}\}/g, data.recipientName ?? '') - .replace(/\{\{ttlHours\}\}/g, String(data.ttlHours)) - : `Activate your ${data.portName} client portal account`; - const greeting = data.recipientName ? `Dear ${escapeHtml(data.recipientName)},` : 'Welcome,'; - const accent = brandingPrimaryColor(overrides?.branding); - - const body = ` -

- Welcome to ${escapeHtml(data.portName)} -

-

${greeting}

-

- You've been invited to access the ${escapeHtml(data.portName)} client portal. - Click the button below to set your password and activate your account. - The link expires in ${data.ttlHours} hours. -

-

- - Activate account - -

-

- If the button doesn't work, paste this link into your browser:
- ${data.link} -

-

- Thank you,
- ${escapeHtml(data.portName)} CRM -

`; - - const text = [ - `Welcome to ${data.portName}`, - '', - `You've been invited to access the ${data.portName} client portal.`, - `Activate your account by visiting: ${data.link}`, - '', - `The link expires in ${data.ttlHours} hours.`, - '', - `Thank you,`, - `${data.portName} CRM`, - ].join('\n'); - - return { - subject, - html: renderShell({ title: subject, body, branding: overrides?.branding }), - text, - }; -} - -export function resetEmail( - data: ResetData, - overrides?: RenderOpts, -): { subject: string; html: string; text: string } { - const subject = overrides?.subject - ? overrides.subject - .replace(/\{\{portName\}\}/g, data.portName) - .replace(/\{\{recipientName\}\}/g, data.recipientName ?? '') - .replace(/\{\{ttlMinutes\}\}/g, String(data.ttlMinutes)) - : `Reset your ${data.portName} client portal password`; - const greeting = data.recipientName ? `Dear ${escapeHtml(data.recipientName)},` : 'Hello,'; - const accent = brandingPrimaryColor(overrides?.branding); - - const body = ` -

- Password reset -

-

${greeting}

-

- We received a request to reset the password on your ${escapeHtml(data.portName)} - client portal account. Click the button below to choose a new one. - The link expires in ${data.ttlMinutes} minutes. -

-

- - Reset password - -

-

- If you didn't request this, you can safely ignore this email - your password will remain unchanged. -

-

- Thank you,
- ${escapeHtml(data.portName)} CRM -

`; - - const text = [ - `Password reset for ${data.portName}`, - '', - `Reset your password by visiting: ${data.link}`, - `The link expires in ${data.ttlMinutes} minutes.`, - '', - `If you didn't request this, you can safely ignore this email.`, - '', - `Thank you,`, - `${data.portName} CRM`, - ].join('\n'); - - return { - subject, - html: renderShell({ title: subject, body, branding: overrides?.branding }), - text, - }; -} - -function escapeHtml(str: string): string { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} diff --git a/src/lib/email/templates/portal-auth.tsx b/src/lib/email/templates/portal-auth.tsx new file mode 100644 index 00000000..02248375 --- /dev/null +++ b/src/lib/email/templates/portal-auth.tsx @@ -0,0 +1,223 @@ +import { render } from '@react-email/components'; +import { Button, Hr, Link, Text } from '@react-email/components'; +import * as React from 'react'; + +import { brandingPrimaryColor, renderShell, safeUrl, type BrandingShell } from '@/lib/email/shell'; + +interface ActivationData { + portName: string; + link: string; + ttlHours: number; + recipientName?: string; +} + +interface ResetData { + portName: string; + link: string; + ttlMinutes: number; + recipientName?: string; +} + +interface RenderOpts { + subject?: string | null; + branding?: BrandingShell | null; +} + +// ─── React-email body components ────────────────────────────────────────────── + +// react-email's `render()` auto-escapes string interpolation, so we don't +// need our hand-rolled escapeHtml() on these bodies. Inline styles use +// camelCase per CSSProperties — react-email serialises them to +// email-client-safe inline `style="..."` attributes on output. + +function ActivationBody({ + portName, + link, + ttlHours, + recipientName, + accent, +}: ActivationData & { accent: string }) { + const greeting = recipientName ? `Dear ${recipientName},` : 'Welcome,'; + + return ( + <> + + Welcome to {portName} + + {greeting} + + You've been invited to access the {portName} client portal. Click the button below to + set your password and activate your account. The link expires in {ttlHours} hours. + +
+ +
+
+ + If the button doesn't work, paste this link into your browser: +
+ + {link} + +
+ + Thank you, +
+ {portName} CRM +
+ + ); +} + +function ResetBody({ + portName, + link, + ttlMinutes, + recipientName, + accent, +}: ResetData & { accent: string }) { + const greeting = recipientName ? `Dear ${recipientName},` : 'Hello,'; + + return ( + <> + + Password reset + + {greeting} + + We received a request to reset the password on your {portName} client portal account. Click + the button below to choose a new one. The link expires in {ttlMinutes} minutes. + +
+ +
+
+ + If you didn't request this, you can safely ignore this email — your password will + remain unchanged. + + + Thank you, +
+ {portName} CRM +
+ + ); +} + +// ─── Public surface ─────────────────────────────────────────────────────────── + +export async function activationEmail( + data: ActivationData, + overrides?: RenderOpts, +): Promise<{ subject: string; html: string; text: string }> { + const subject = overrides?.subject + ? overrides.subject + .replace(/\{\{portName\}\}/g, data.portName) + .replace(/\{\{recipientName\}\}/g, data.recipientName ?? '') + .replace(/\{\{ttlHours\}\}/g, String(data.ttlHours)) + : `Activate your ${data.portName} client portal account`; + const accent = brandingPrimaryColor(overrides?.branding); + + const body = await render(, { + pretty: false, + }); + + const text = [ + `Welcome to ${data.portName}`, + '', + `You've been invited to access the ${data.portName} client portal.`, + `Activate your account by visiting: ${data.link}`, + '', + `The link expires in ${data.ttlHours} hours.`, + '', + `Thank you,`, + `${data.portName} CRM`, + ].join('\n'); + + return { + subject, + html: renderShell({ title: subject, body, branding: overrides?.branding }), + text, + }; +} + +export async function resetEmail( + data: ResetData, + overrides?: RenderOpts, +): Promise<{ subject: string; html: string; text: string }> { + const subject = overrides?.subject + ? overrides.subject + .replace(/\{\{portName\}\}/g, data.portName) + .replace(/\{\{recipientName\}\}/g, data.recipientName ?? '') + .replace(/\{\{ttlMinutes\}\}/g, String(data.ttlMinutes)) + : `Reset your ${data.portName} client portal password`; + const accent = brandingPrimaryColor(overrides?.branding); + + const body = await render(, { + pretty: false, + }); + + const text = [ + `Password reset for ${data.portName}`, + '', + `Reset your password by visiting: ${data.link}`, + `The link expires in ${data.ttlMinutes} minutes.`, + '', + `If you didn't request this, you can safely ignore this email.`, + '', + `Thank you,`, + `${data.portName} CRM`, + ].join('\n'); + + return { + subject, + html: renderShell({ title: subject, body, branding: overrides?.branding }), + text, + }; +} diff --git a/src/lib/services/portal-auth.service.ts b/src/lib/services/portal-auth.service.ts index 07a56f6f..8116fad9 100644 --- a/src/lib/services/portal-auth.service.ts +++ b/src/lib/services/portal-auth.service.ts @@ -147,7 +147,7 @@ async function issueActivationToken( const link = `${env.APP_URL}/portal/activate?token=${encodeURIComponent(raw)}`; const subjectOverride = await loadSubjectOverride(portId, 'portal_activation'); const branding = await getBrandingShell(portId); - const { subject, html, text } = activationEmail( + const { subject, html, text } = await activationEmail( { portName, link, @@ -408,7 +408,7 @@ export async function requestPasswordReset(email: string): Promise { const link = `${env.APP_URL}/portal/reset-password?token=${encodeURIComponent(raw)}`; const subjectOverride = await loadSubjectOverride(user.portId, 'portal_reset'); const branding = await getBrandingShell(user.portId); - const { subject, html, text } = resetEmail( + const { subject, html, text } = await resetEmail( { portName, link, diff --git a/vitest.config.ts b/vitest.config.ts index 2b7a5885..f4f8f351 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,9 +1,16 @@ import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; import path from 'path'; // eslint-disable-next-line @typescript-eslint/no-require-imports const { loadEnv } = require('vite'); export default defineConfig({ + // Next.js tsconfig sets jsx: 'preserve' so .tsx files imported by + // tests (e.g. react-email templates) aren't transformed by vite's + // default loader. The official react plugin handles the JSX + // transform in test-time only — Next's runtime keeps its preserve + // setting for the prod build. + plugins: [react()], test: { globals: true, environment: 'node',