diff --git a/package.json b/package.json index 867e9b13..a6c156e2 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,9 @@ "react-dom": "^18", "react-hook-form": "^7.52.2", "react-json-editor-ajrm": "^2.5.14", + "react-markdown": "^9.0.1", "recharts": "^2.12.7", + "remark-gfm": "^4.0.0", "sonner": "^1.5.0", "swiper": "^11.1.9", "tailwind-merge": "^2.4.0", @@ -54,6 +56,7 @@ "devDependencies": { "@codemirror/lang-javascript": "^6.2.2", "@svgr/webpack": "^8.1.0", + "@tailwindcss/typography": "^0.5.15", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0430aa9..905f43b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -103,9 +103,15 @@ importers: react-json-editor-ajrm: specifier: ^2.5.14 version: 2.5.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-markdown: + specifier: ^9.0.1 + version: 9.0.1(@types/react@18.3.3)(react@18.3.1) recharts: specifier: ^2.12.7 version: 2.12.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + remark-gfm: + specifier: ^4.0.0 + version: 4.0.0 sonner: specifier: ^1.5.0 version: 1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -125,6 +131,9 @@ importers: "@svgr/webpack": specifier: ^8.1.0 version: 8.1.0(typescript@5.5.4) + "@tailwindcss/typography": + specifier: ^0.5.15 + version: 0.5.15(tailwindcss@3.4.6) "@types/node": specifier: ^20 version: 20.14.12 @@ -2662,6 +2671,14 @@ packages: integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==, } + "@tailwindcss/typography@0.5.15": + resolution: + { + integrity: sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==, + } + peerDependencies: + tailwindcss: ">=3.0.0 || insiders || >=4.0.0-alpha.20" + "@tanstack/react-table@8.20.1": resolution: { @@ -2740,6 +2757,24 @@ packages: integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==, } + "@types/debug@4.1.12": + resolution: + { + integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==, + } + + "@types/estree-jsx@1.0.5": + resolution: + { + integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==, + } + + "@types/estree@1.0.5": + resolution: + { + integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==, + } + "@types/hast@2.3.10": resolution: { @@ -2764,6 +2799,12 @@ packages: integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==, } + "@types/ms@0.7.34": + resolution: + { + integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==, + } + "@types/node@20.14.12": resolution: { @@ -4005,6 +4046,13 @@ packages: } engines: { node: ">=10" } + escape-string-regexp@5.0.0: + resolution: + { + integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==, + } + engines: { node: ">=12" } + eslint-config-next@14.2.5: resolution: { @@ -4156,6 +4204,12 @@ packages: } engines: { node: ">=4.0" } + estree-util-is-identifier-name@3.0.0: + resolution: + { + integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==, + } + esutils@2.0.3: resolution: { @@ -4536,6 +4590,12 @@ packages: integrity: sha512-hZOofyZANbyWo+9RP75xIDV/gq+OUKx+T46IlwERnKmfpwp81XBFbT9mi26ws+SJchA4RVUQwIBJpqEOBhMzEQ==, } + hast-util-to-jsx-runtime@2.3.0: + resolution: + { + integrity: sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ==, + } + hast-util-to-parse5@8.0.0: resolution: { @@ -4573,6 +4633,12 @@ packages: } engines: { node: ">=14" } + html-url-attributes@3.0.0: + resolution: + { + integrity: sha512-/sXbVCWayk6GDVg3ctOX6nxaVj7So40FcFAnWlWGNAB1LpYKcV5Cd10APjPjW80O7zYW2MsjBV4zZ7IZO5fVow==, + } + html-void-elements@3.0.0: resolution: { @@ -4631,6 +4697,12 @@ packages: integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==, } + inline-style-parser@0.2.4: + resolution: + { + integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==, + } + internal-slot@1.0.7: resolution: { @@ -5081,12 +5153,24 @@ packages: } engines: { node: ">=10" } + lodash.castarray@4.4.0: + resolution: + { + integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==, + } + lodash.debounce@4.0.8: resolution: { integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==, } + lodash.isplainobject@4.0.6: + resolution: + { + integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==, + } + lodash.merge@4.6.2: resolution: { @@ -5099,6 +5183,12 @@ packages: integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==, } + longest-streak@3.1.0: + resolution: + { + integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==, + } + loose-envify@1.4.0: resolution: { @@ -5139,6 +5229,12 @@ packages: } engines: { node: ">=18.0.0" } + markdown-table@3.0.3: + resolution: + { + integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==, + } + marked@7.0.4: resolution: { @@ -5155,12 +5251,96 @@ packages: peerDependencies: react: 18.x + mdast-util-find-and-replace@3.0.1: + resolution: + { + integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==, + } + + mdast-util-from-markdown@2.0.1: + resolution: + { + integrity: sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==, + } + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: + { + integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==, + } + + mdast-util-gfm-footnote@2.0.0: + resolution: + { + integrity: sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==, + } + + mdast-util-gfm-strikethrough@2.0.0: + resolution: + { + integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==, + } + + mdast-util-gfm-table@2.0.0: + resolution: + { + integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==, + } + + mdast-util-gfm-task-list-item@2.0.0: + resolution: + { + integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==, + } + + mdast-util-gfm@3.0.0: + resolution: + { + integrity: sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==, + } + + mdast-util-mdx-expression@2.0.1: + resolution: + { + integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==, + } + + mdast-util-mdx-jsx@3.1.3: + resolution: + { + integrity: sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ==, + } + + mdast-util-mdxjs-esm@2.0.1: + resolution: + { + integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==, + } + + mdast-util-phrasing@4.1.0: + resolution: + { + integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==, + } + mdast-util-to-hast@13.2.0: resolution: { integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==, } + mdast-util-to-markdown@2.1.0: + resolution: + { + integrity: sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==, + } + + mdast-util-to-string@4.0.0: + resolution: + { + integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==, + } + mdn-data@2.0.28: resolution: { @@ -5180,24 +5360,156 @@ packages: } engines: { node: ">= 8" } + micromark-core-commonmark@2.0.1: + resolution: + { + integrity: sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==, + } + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: + { + integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==, + } + + micromark-extension-gfm-footnote@2.1.0: + resolution: + { + integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==, + } + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: + { + integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==, + } + + micromark-extension-gfm-table@2.1.0: + resolution: + { + integrity: sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==, + } + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: + { + integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==, + } + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: + { + integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==, + } + + micromark-extension-gfm@3.0.0: + resolution: + { + integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==, + } + + micromark-factory-destination@2.0.0: + resolution: + { + integrity: sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==, + } + + micromark-factory-label@2.0.0: + resolution: + { + integrity: sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==, + } + + micromark-factory-space@2.0.0: + resolution: + { + integrity: sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==, + } + + micromark-factory-title@2.0.0: + resolution: + { + integrity: sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==, + } + + micromark-factory-whitespace@2.0.0: + resolution: + { + integrity: sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==, + } + micromark-util-character@2.1.0: resolution: { integrity: sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==, } + micromark-util-chunked@2.0.0: + resolution: + { + integrity: sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==, + } + + micromark-util-classify-character@2.0.0: + resolution: + { + integrity: sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==, + } + + micromark-util-combine-extensions@2.0.0: + resolution: + { + integrity: sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==, + } + + micromark-util-decode-numeric-character-reference@2.0.1: + resolution: + { + integrity: sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==, + } + + micromark-util-decode-string@2.0.0: + resolution: + { + integrity: sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==, + } + micromark-util-encode@2.0.0: resolution: { integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==, } + micromark-util-html-tag-name@2.0.0: + resolution: + { + integrity: sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==, + } + + micromark-util-normalize-identifier@2.0.0: + resolution: + { + integrity: sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==, + } + + micromark-util-resolve-all@2.0.0: + resolution: + { + integrity: sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==, + } + micromark-util-sanitize-uri@2.0.0: resolution: { integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==, } + micromark-util-subtokenize@2.0.1: + resolution: + { + integrity: sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==, + } + micromark-util-symbol@2.0.0: resolution: { @@ -5210,6 +5522,12 @@ packages: integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==, } + micromark@4.0.0: + resolution: + { + integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==, + } + micromatch@4.0.7: resolution: { @@ -5648,6 +5966,13 @@ packages: peerDependencies: postcss: ^8.2.14 + postcss-selector-parser@6.0.10: + resolution: + { + integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==, + } + engines: { node: ">=4" } + postcss-selector-parser@6.1.1: resolution: { @@ -5796,6 +6121,15 @@ packages: react: ">=16.2.0" react-dom: ">=16.2.0" + react-markdown@9.0.1: + resolution: + { + integrity: sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==, + } + peerDependencies: + "@types/react": ">=18" + react: ">=18" + react-promise-suspense@0.3.4: resolution: { @@ -5984,6 +6318,30 @@ packages: integrity: sha512-AcSLS2mItY+0fYu9xKxOu1LhUZeBZZBx8//5HKzF+0XP+eP8+6a5MXn2+DW2kfXR6Dtp1FEXMVrjyKAcvcU8vg==, } + remark-gfm@4.0.0: + resolution: + { + integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==, + } + + remark-parse@11.0.0: + resolution: + { + integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==, + } + + remark-rehype@11.1.0: + resolution: + { + integrity: sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==, + } + + remark-stringify@11.0.0: + resolution: + { + integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==, + } + resolve-from@4.0.0: resolution: { @@ -6257,6 +6615,12 @@ packages: integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==, } + style-to-object@1.0.8: + resolution: + { + integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==, + } + styled-jsx@5.1.1: resolution: { @@ -8818,6 +9182,14 @@ snapshots: "@swc/counter": 0.1.3 tslib: 2.6.3 + "@tailwindcss/typography@0.5.15(tailwindcss@3.4.6)": + dependencies: + lodash.castarray: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + postcss-selector-parser: 6.0.10 + tailwindcss: 3.4.6 + "@tanstack/react-table@8.20.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": dependencies: "@tanstack/table-core": 8.20.1 @@ -8852,6 +9224,16 @@ snapshots: "@types/d3-timer@3.0.2": {} + "@types/debug@4.1.12": + dependencies: + "@types/ms": 0.7.34 + + "@types/estree-jsx@1.0.5": + dependencies: + "@types/estree": 1.0.5 + + "@types/estree@1.0.5": {} + "@types/hast@2.3.10": dependencies: "@types/unist": 2.0.11 @@ -8866,6 +9248,8 @@ snapshots: dependencies: "@types/unist": 3.0.3 + "@types/ms@0.7.34": {} + "@types/node@20.14.12": dependencies: undici-types: 5.26.5 @@ -9722,6 +10106,8 @@ snapshots: escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} + eslint-config-next@14.2.5(eslint@8.57.0)(typescript@5.5.4): dependencies: "@next/eslint-plugin-next": 14.2.5 @@ -9929,6 +10315,8 @@ snapshots: estraverse@5.3.0: {} + estree-util-is-identifier-name@3.0.0: {} + esutils@2.0.3: {} eventemitter3@4.0.7: {} @@ -10188,6 +10576,26 @@ snapshots: stringify-entities: 4.0.4 zwitch: 2.0.4 + hast-util-to-jsx-runtime@2.3.0: + dependencies: + "@types/estree": 1.0.5 + "@types/hast": 3.0.4 + "@types/unist": 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.1.3 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + style-to-object: 1.0.8 + unist-util-position: 5.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + hast-util-to-parse5@8.0.0: dependencies: "@types/hast": 3.0.4 @@ -10230,6 +10638,8 @@ snapshots: htmlparser2: 8.0.2 selderee: 0.11.0 + html-url-attributes@3.0.0: {} + html-void-elements@3.0.0: {} htmlparser2@8.0.2: @@ -10259,6 +10669,8 @@ snapshots: ini@1.3.8: {} + inline-style-parser@0.2.4: {} + internal-slot@1.0.7: dependencies: es-errors: 1.3.0 @@ -10485,12 +10897,18 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.castarray@4.4.0: {} + lodash.debounce@4.0.8: {} + lodash.isplainobject@4.0.6: {} + lodash.merge@4.6.2: {} lodash@4.17.21: {} + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -10517,6 +10935,8 @@ snapshots: transitivePeerDependencies: - debug + markdown-table@3.0.3: {} + marked@7.0.4: {} md-to-react-email@5.0.2(react@18.3.1): @@ -10524,6 +10944,131 @@ snapshots: marked: 7.0.4 react: 18.3.1 + mdast-util-find-and-replace@3.0.1: + dependencies: + "@types/mdast": 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + mdast-util-from-markdown@2.0.1: + dependencies: + "@types/mdast": 4.0.4 + "@types/unist": 3.0.3 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-decode-string: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + "@types/mdast": 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.1 + micromark-util-character: 2.1.0 + + mdast-util-gfm-footnote@2.0.0: + dependencies: + "@types/mdast": 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + micromark-util-normalize-identifier: 2.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + "@types/mdast": 4.0.4 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + "@types/mdast": 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.3 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + "@types/mdast": 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.1 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.0.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + "@types/estree-jsx": 1.0.5 + "@types/hast": 3.0.4 + "@types/mdast": 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.1.3: + dependencies: + "@types/estree-jsx": 1.0.5 + "@types/hast": 3.0.4 + "@types/mdast": 4.0.4 + "@types/unist": 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + parse-entities: 4.0.1 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + "@types/estree-jsx": 1.0.5 + "@types/hast": 3.0.4 + "@types/mdast": 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + "@types/mdast": 4.0.4 + unist-util-is: 6.0.0 + mdast-util-to-hast@13.2.0: dependencies: "@types/hast": 3.0.4 @@ -10536,29 +11081,218 @@ snapshots: unist-util-visit: 5.0.0 vfile: 6.0.2 + mdast-util-to-markdown@2.1.0: + dependencies: + "@types/mdast": 4.0.4 + "@types/unist": 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-decode-string: 2.0.0 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + "@types/mdast": 4.0.4 + mdn-data@2.0.28: {} mdn-data@2.0.30: {} merge2@1.4.1: {} + micromark-core-commonmark@2.0.1: + dependencies: + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-factory-destination: 2.0.0 + micromark-factory-label: 2.0.0 + micromark-factory-space: 2.0.0 + micromark-factory-title: 2.0.0 + micromark-factory-whitespace: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-classify-character: 2.0.0 + micromark-util-html-tag-name: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-subtokenize: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.1 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-classify-character: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-table@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.0 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.0 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-destination@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-label@2.0.0: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-space@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-types: 2.0.0 + + micromark-factory-title@2.0.0: + dependencies: + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-whitespace@2.0.0: + dependencies: + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + micromark-util-character@2.1.0: dependencies: micromark-util-symbol: 2.0.0 micromark-util-types: 2.0.0 + micromark-util-chunked@2.0.0: + dependencies: + micromark-util-symbol: 2.0.0 + + micromark-util-classify-character@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-combine-extensions@2.0.0: + dependencies: + micromark-util-chunked: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-decode-numeric-character-reference@2.0.1: + dependencies: + micromark-util-symbol: 2.0.0 + + micromark-util-decode-string@2.0.0: + dependencies: + decode-named-character-reference: 1.0.2 + micromark-util-character: 2.1.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-encode@2.0.0: {} + micromark-util-html-tag-name@2.0.0: {} + + micromark-util-normalize-identifier@2.0.0: + dependencies: + micromark-util-symbol: 2.0.0 + + micromark-util-resolve-all@2.0.0: + dependencies: + micromark-util-types: 2.0.0 + micromark-util-sanitize-uri@2.0.0: dependencies: micromark-util-character: 2.1.0 micromark-util-encode: 2.0.0 micromark-util-symbol: 2.0.0 + micromark-util-subtokenize@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + micromark-util-symbol@2.0.0: {} micromark-util-types@2.0.0: {} + micromark@4.0.0: + dependencies: + "@types/debug": 4.1.12 + debug: 4.3.5 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.1 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-combine-extensions: 2.0.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-encode: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-subtokenize: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + transitivePeerDependencies: + - supports-color + micromatch@4.0.7: dependencies: braces: 3.0.3 @@ -10812,6 +11546,11 @@ snapshots: postcss: 8.4.39 postcss-selector-parser: 6.1.1 + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss-selector-parser@6.1.1: dependencies: cssesc: 3.0.0 @@ -10886,6 +11625,23 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-markdown@9.0.1(@types/react@18.3.3)(react@18.3.1): + dependencies: + "@types/hast": 3.0.4 + "@types/react": 18.3.3 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.0 + html-url-attributes: 3.0.0 + mdast-util-to-hast: 13.2.0 + react: 18.3.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.0 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.2 + transitivePeerDependencies: + - supports-color + react-promise-suspense@0.3.4: dependencies: fast-deep-equal: 2.0.1 @@ -11048,6 +11804,40 @@ snapshots: rehype-stringify: 10.0.0 unified: 11.0.5 + remark-gfm@4.0.0: + dependencies: + "@types/mdast": 4.0.4 + mdast-util-gfm: 3.0.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + "@types/mdast": 4.0.4 + mdast-util-from-markdown: 2.0.1 + micromark-util-types: 2.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.0: + dependencies: + "@types/hast": 3.0.4 + "@types/mdast": 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.2 + + remark-stringify@11.0.0: + dependencies: + "@types/mdast": 4.0.4 + mdast-util-to-markdown: 2.1.0 + unified: 11.0.5 + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -11227,6 +12017,10 @@ snapshots: style-mod@4.1.2: {} + style-to-object@1.0.8: + dependencies: + inline-style-parser: 0.2.4 + styled-jsx@5.1.1(@babel/core@7.25.2)(react@18.3.1): dependencies: client-only: 0.0.1 diff --git a/src/app/globals.css b/src/app/globals.css index 15138e71..41fb2f1b 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -208,3 +208,17 @@ tbody { transform: translate(0, 0); } } + +*::-webkit-scrollbar { + display: none; +} + +* { + scrollbar-width: none; +} + +/* Hide scrollbar for IE, Edge and Firefox */ +* { + -ms-overflow-style: none; + scrollbar-width: none; +} diff --git a/src/app/portal/admin/actions.ts b/src/app/portal/admin/actions.ts index 88521150..ef38c143 100644 --- a/src/app/portal/admin/actions.ts +++ b/src/app/portal/admin/actions.ts @@ -2,6 +2,9 @@ import { db } from "@/db"; import { FormStep } from "@/lib/types/questions"; import { FormFields } from "@/app/portal/admin/forms/[id]/submissions/columns"; +import { sendEmail } from "@/lib/utils/forms/email"; +import { MarkdownTemplate } from "@/components/forms/emailTemplates/markdownTemplate"; +import { render } from "@react-email/components"; export async function getForms() { return db.forms.findMany(); @@ -11,7 +14,7 @@ export async function createForm(data: { title: string; description: string }) { return db.forms.create({ data: { ...data, config: {}, questions: [] } }); } -export async function getForm(id: number) { +export async function getForm(id: number | bigint) { try { return db.forms.findFirst({ where: { @@ -162,14 +165,32 @@ export async function updateSubmissionField( field: string, tableName: string | undefined, value: any, + cta: boolean = false, ) { if (tableName === "applications") { - return db["applications"].update({ + await db["applications"].update({ where: { id: submissionId }, data: { [field]: value, }, }); + if (field === "status" && cta) { + const submission = await db.submissions.findFirst({ + where: { + id: submissionId, + }, + }); + + if (!submission) { + console.log("Submission not found"); + return; + } + await sendStatusEmail({ + status: value, + formId: submission.form_id, + userId: submission.user_id, + }); + } } } @@ -194,3 +215,65 @@ export async function getAdminMembers() { }; }); } + +async function sendStatusEmail({ + status, + formId, + userId, +}: { + status: string; + formId: bigint; + userId: string; +}) { + const form = await getForm(formId); + + if (!form) { + console.log("Form not found"); + return; + } + const app = await db.submissions.findFirst({ + where: { + form_id: formId, + user_id: userId, + }, + include: { + applications: true, + users: true, + }, + }); + + if (!app) { + console.log("Application not found"); + return; + } + + const formConfig = form.config as any; + const config = formConfig.application as any; + + if ( + !config || + !config.emails || + !config.emails.status || + !config.emails.status[status] + ) { + console.log("Email template not found"); + return; + } + + const emailTemplate = config.emails.status[status]; + const title = emailTemplate.title; + const content = emailTemplate.content; + const details = app.details ? (app.details as any) : {}; + const template = await render( + MarkdownTemplate({ markdown: content, replacements: details }), + ); + + await sendEmail({ + from: "no-reply@ubclaunchpad.com", + fromName: "no-reply UBC Launch Pad", + to: app.users.email!.toString(), + subject: title, + html: template, + cc: details?.email as string, + }); +} diff --git a/src/app/portal/admin/forms/[id]/submissions/columns.tsx b/src/app/portal/admin/forms/[id]/submissions/columns.tsx index fe43f2a0..60df6ba6 100644 --- a/src/app/portal/admin/forms/[id]/submissions/columns.tsx +++ b/src/app/portal/admin/forms/[id]/submissions/columns.tsx @@ -1,11 +1,17 @@ import { ColumnDef, Row } from "@tanstack/react-table"; -import { useContext, useState } from "react"; +import { useContext, useEffect, useState } from "react"; import MultiSelect from "@/components/general/multiSelect"; import { updateSubmissionField } from "@/app/portal/admin/actions"; import { toast } from "sonner"; import FloatingTextArea from "@/components/primitives/floatingTextArea"; import { Button } from "@/components/primitives/button"; import { formContext } from "@/components/layouts/formTabView"; +import { + CircleCheckIcon, + LoaderCircleIcon, + MailIcon, + Maximize2Icon, +} from "lucide-react"; export type FormFields = { [key: string]: { @@ -258,22 +264,13 @@ export function createColumns( setAndOpen({ applicant: row }); }} className={ - " h-fit bg-background-500 rounded-md p-2 w-fit flex border border-transparent hover:border-background-500 items-center justify-center gap-2 " + " h-fit bg-background-500 rounded-md p-2 w-fit flex border border-transparent hover:border-background-500 items-center justify-center gap-2 " } > + View - + ); }, @@ -282,6 +279,62 @@ export function createColumns( ]; } +function NotifyButtonForEmail({ row }: { row: any }) { + const [emailState, setEmailState] = useState<"idle" | "loading" | "sent">( + "idle", + ); + + useEffect(() => { + if (emailState === "sent") { + const timer = setTimeout(() => { + setEmailState("idle"); + }, 5000); + + return () => { + clearTimeout(timer); + }; + } + }, [emailState]); + + return ( + + ); +} + function FieldPopover({ value, }: { diff --git a/src/app/portal/api/v1/offers/[id]/route.ts b/src/app/portal/api/v1/offers/[id]/route.ts index f93fa61a..21356a55 100644 --- a/src/app/portal/api/v1/offers/[id]/route.ts +++ b/src/app/portal/api/v1/offers/[id]/route.ts @@ -3,14 +3,15 @@ import { z } from "zod"; import { db } from "@/db"; const newOfferSchema = z.object({ - userId: z.string().uuid(), - teamId: z.number(), - appId: z.string().uuid(), - expiringAt: z.date().optional(), + status: z.enum(["accepted", "declined", "offered", "expired"]), }); -export async function POST(request: NextRequest) { - const offerDetails = newOfferSchema.safeParse(request.body); +export async function POST( + request: NextRequest, + { params }: { params: { id: string } }, +) { + const reqBody = await request.json(); + const offerDetails = newOfferSchema.safeParse(reqBody); if (!offerDetails.success) { return NextResponse.json( { message: "Invalid offer details" }, @@ -18,7 +19,7 @@ export async function POST(request: NextRequest) { ); } - const { userId, teamId, appId } = offerDetails.data; + const appId = params.id; const application = await db.applications.findUnique({ where: { @@ -29,27 +30,88 @@ export async function POST(request: NextRequest) { }, }); - if (!application) { + if ( + !application || + !application.submissions || + !application.submissions.details + ) { return NextResponse.json("Application not found", { status: 404 }); } - await db.pending_members.create({ - data: { - user_id: userId, - team_id: teamId, - status: "pending", - meta: { - application: application, - expiringAt: offerDetails.data.expiringAt, - }, - }, - }); - - const body = { - message: `Offer to join has been created`, - }; + try { + await db.$transaction(async (transaction) => { + // Update application status + await transaction.applications.update({ + where: { id: appId }, + data: { status: offerDetails.data.status }, + }); + + const userId = application.submissions.user_id; + const teamId = application.team_id; + const details = application.submissions.details as { + [key: string]: string; + }; + + // If the offer is declined, return early + if (offerDetails.data.status === "declined") { + const body = { message: `Offer to join has been declined` }; + return NextResponse.json(JSON.stringify(body), { status: 200 }); + } + + if (offerDetails.data.status === "expired") { + const body = { message: `Offer to join has expired` }; + return NextResponse.json(JSON.stringify(body), { status: 200 }); + } + + if (offerDetails.data.status === "offered") { + const body = { message: `Offer to join has been offered` }; + return NextResponse.json(JSON.stringify(body), { status: 200 }); + } + + // Upsert member details + await transaction.members.upsert({ + where: { id: userId }, + create: { + id: userId, + first_name: details["firstName"] ?? "", + last_name: details["lastName"] ?? "", + faculty: details["faculty"]?.[0] ?? "", + specialization: details["specialization"]?.[0] ?? "", + grad_year: Number(details["graduationYear"]) ?? null, + year_level: Number(details["yearLevel"]?.[0]) ?? null, + }, + update: { + first_name: details["firstName"] ?? "", + last_name: details["lastName"] ?? "", + faculty: details["faculty"]?.[0] ?? "", + specialization: details["specialization"]?.[0] ?? "", + grad_year: Number(details["graduationYear"]) ?? null, + year_level: Number(details["yearLevel"]?.[0]) ?? null, + }, + }); + + // Create team member if teamId is provided + if (teamId) { + await transaction.team_members.create({ + data: { + team_id: teamId, + member_id: userId, + role: details["role"]?.[0] ?? "", + }, + }); + } + }); - return NextResponse.json(JSON.stringify(body), { status: 201 }); + const body = { message: `Offer to join has been created` }; + return NextResponse.json(JSON.stringify(body), { status: 200 }); + } catch (error) { + console.error("Transaction failed:", error); + const body = { + message: `Failed to process the offer`, + error: error.message, + }; + return NextResponse.json(JSON.stringify(body), { status: 500 }); + } } export async function GET( @@ -73,7 +135,7 @@ const updateOfferSchema = z.object({ status: z.enum(["accepted", "declined", "expired"]), }); -export async function PUT( +export async function PATCH( request: NextRequest, { params }: { params: { id: string } }, ) { @@ -124,3 +186,7 @@ export async function PUT( return NextResponse.json(JSON.stringify(body), { status: 200 }); } + +function camelToSnake(str: string) { + return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); +} diff --git a/src/app/portal/forms/[id]/application/page.tsx b/src/app/portal/forms/[id]/application/page.tsx new file mode 100644 index 00000000..dc7838e7 --- /dev/null +++ b/src/app/portal/forms/[id]/application/page.tsx @@ -0,0 +1,70 @@ +import { redirect } from "next/navigation"; +import { GenericResult, AcceptedResult } from "@/components/forms/resultPages"; +import { getForm } from "@/app/portal/admin/actions"; +import { getUserApplication } from "@/app/portal/forms/actions"; +import { Form } from "@/lib/types/application"; +import GenericGreeter from "@/components/layouts/genericGreeter"; +import OfferPage from "@/components/forms/applications/offerPage"; + +const text = { + closed: "This form is now closed.", + submitted: "Your application has been submitted.", + rejected: + "Unfortunately, it looks like your application was not successful this time. However, we encourage you to apply again in the future.", + default: "No longer available", +}; + +export default async function page({ + params, +}: { + params: { [key: string]: string }; +}) { + if (!params.id) { + redirect("/portal/forms"); + } + const formP = getForm(Number(params.id)) as unknown as Promise
; + const appP = getUserApplication({ + formId: Number(params.id) as unknown as bigint, + includeApp: true, + }); + const [form, app] = await Promise.all([formP, appP]); + + if (!form) { + redirect("/portal/forms"); + } + if (!app || !app.applications) { + redirect("/portal/forms"); + } + + const userApp = app.applications; + const status = userApp.status; + let subpage = null; + + switch (status) { + case "submitted": + case "rejected": + subpage = renderTerminalPage(status, form); + break; + case "accepted": + case "declined": + case "offered": + subpage = ; + break; + case "pending": + default: + subpage = null; + } + return {subpage}; +} + +function renderTerminalPage(status: string, form: any) { + const terminalStatus = ["rejected", "submitted"]; + if (terminalStatus.includes(status)) { + switch (status) { + case "rejected": + return ; + case "submitted": + return ; + } + } +} diff --git a/src/app/portal/forms/actions.ts b/src/app/portal/forms/actions.ts index 94b5cbe7..f235f798 100644 --- a/src/app/portal/forms/actions.ts +++ b/src/app/portal/forms/actions.ts @@ -64,8 +64,6 @@ export async function submitApplication({ formId }: { formId: bigint }) { const promises = trigger.actions.map(async (action) => { if (action.type === "api") { const body = replaceTemplateValues(action.body, res.details); - console.log(`${process.env.NEXT_PUBLIC_BASE_URL}${action.url}`); - console.log(body); const response = await fetch( `${process.env.NEXT_PUBLIC_BASE_URL}${action.url}`, { @@ -75,14 +73,6 @@ export async function submitApplication({ formId }: { formId: bigint }) { }, ); const data = await response.json(); - console.log(data); - // action.responseHandlers.forEach((handler) => { - // if (handler.type === "success") { - // console.log(handler.message, data); - // } else { - // console.error(handler.message, data); - // } - // }); } }); Promise.all(promises); @@ -168,7 +158,13 @@ export async function updateApplication({ return res; } -export async function getUserApplication({ formId }: { formId: bigint }) { +export async function getUserApplication({ + formId, + includeApp, +}: { + formId: bigint; + includeApp?: boolean; +}) { const supabase = createClient(); const { data, error } = await supabase.auth.getUser(); if (!data.user || error) { @@ -181,6 +177,9 @@ export async function getUserApplication({ formId }: { formId: bigint }) { user_id: data.user.id, }, }, + include: { + applications: includeApp, + }, }); } diff --git a/src/components/forms/applications/offerPage.tsx b/src/components/forms/applications/offerPage.tsx new file mode 100644 index 00000000..f505a1a8 --- /dev/null +++ b/src/components/forms/applications/offerPage.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { GenericResult } from "@/components/forms/resultPages"; +import { Form } from "@/lib/types/application"; +import { Button } from "@/components/primitives/button"; +import { useState } from "react"; + +export default function OfferPage({ form, app }: { form: Form; app: any }) { + const [reqStatus, setReqStatus] = useState<"idle" | "loading" | "error">( + "idle", + ); + const [status, setStatus] = useState< + "offered" | "accepted" | "declined" | "error" + >(app.applications.status); + const applicationConfig = form.config.application; + const page = applicationConfig.pages["offerPage"]; + const text = page.content; + const cleanedText = replaceTemplateValues(text, app.details); + + async function handleDecision(decision: "accepted" | "declined") { + setReqStatus("loading"); + const res = await fetch(`/portal/api/v1/offers/${app.applications.id}`, { + method: "POST", + body: JSON.stringify({ status: decision }), + }); + if (res.ok) { + setStatus(decision); + setReqStatus("idle"); + } else { + setStatus("error"); + setReqStatus("error"); + } + } + + return ( + <> + {(() => { + switch (status) { + case "error": + return ( +
+ There was an error processing your request. Please try again + later. +
+ ); + case "declined": + return ( +
+ + You have declined the offer :( + +
+ ); + default: + return ( + + {status === "accepted" ? ( +
+ + You have accepted the offer! 🎉 + +
+ ) : ( +
+ {reqStatus === "loading" && ( +
+ + Sending the right signal to the server... + +
+ )} +
+ + +
+
+ )} +
+ ); + } + })()} + + ); +} + +function replaceTemplateValues(text: string, values: any) { + const regex = /{{(.*?)}}/g; + const matches = text.match(regex); + if (!matches) { + return text; + } + let newText = text; + matches.forEach((match) => { + const key = match.replace("{{", "").replace("}}", ""); + if (!values[key]) { + newText = newText.replace(match, ""); + } else { + newText = newText.replace(match, values[key]); + } + }); + return newText; +} diff --git a/src/components/forms/emailTemplates/markdownTemplate.tsx b/src/components/forms/emailTemplates/markdownTemplate.tsx new file mode 100644 index 00000000..d49f8557 --- /dev/null +++ b/src/components/forms/emailTemplates/markdownTemplate.tsx @@ -0,0 +1,35 @@ +import { Markdown, Html } from "@react-email/components"; + +export function MarkdownTemplate({ + markdown, + replacements, +}: { + markdown: string; + replacements?: Record; +}) { + const cleanedMarkdown = replaceTemplateValues(markdown, replacements || {}); + + return ( + + {cleanedMarkdown} + + ); +} + +function replaceTemplateValues(text: string, values: any) { + const regex = /{{(.*?)}}/g; + const matches = text.match(regex); + if (!matches) { + return text; + } + let newText = text; + matches.forEach((match) => { + const key = match.replace("{{", "").replace("}}", ""); + if (!values[key]) { + newText = newText.replace(match, ""); + } else { + newText = newText.replace(match, values[key]); + } + }); + return newText; +} diff --git a/src/components/forms/resultPages/genericResult.tsx b/src/components/forms/resultPages/genericResult.tsx index 6b670ff7..3522f4fc 100644 --- a/src/components/forms/resultPages/genericResult.tsx +++ b/src/components/forms/resultPages/genericResult.tsx @@ -1,32 +1,76 @@ import { Form } from "@/lib/types/application"; import Link from "next/link"; +import Markdown from "react-markdown"; + +import remarkGfm from "remark-gfm"; export default function GenericResult({ application, message, + asMarkdown, + children, }: { application: Form; message: string; + asMarkdown?: boolean; + children?: React.ReactNode; }) { return (
-
+

{application.title}

-

{message}

-

- If you have any questions, please contact us at{" "} - ( + + ), + + // link: (props: any) => ( ) + }} + className="rounded-xl dark + + prose prose-lg prose-invert max-w-4xl w-full dark:prose-invert *:text-white" > - strategy@ubclaunchpad.com - - . -

+ {message} + + ) : ( + <> +

{message}

+

+ If you have any questions, please contact us at{" "} + + strategy@ubclaunchpad.com + + . +

+ + )} + {children}
); } + +const CustomLink = ({ href, text }: { href: string; text: string }) => { + return ( + + {text} + + ); +}; diff --git a/src/components/layouts/genericGreeter.tsx b/src/components/layouts/genericGreeter.tsx index da3f9e5b..013b49d4 100644 --- a/src/components/layouts/genericGreeter.tsx +++ b/src/components/layouts/genericGreeter.tsx @@ -19,14 +19,16 @@ export default function GenericGreeter({ src={"/images/assets/planet1.svg"} alt={"planet"} layout={"fill"} + className="hidden xl:block" style={{ objectFit: "contain" }} />
-
+
{"planet"}
@@ -34,14 +36,15 @@ export default function GenericGreeter({ src={"/images/assets/starsBg.svg"} alt={"planet"} layout={"fill"} + className={"sm:opacity-0 md:opacity-15 xl:opacity-35"} style={{ objectFit: "cover" }} />
{includeStyle ? ( -
+
{children} diff --git a/src/lib/utils/helpers.ts b/src/lib/utils/helpers.ts index 7fda2964..d3a55d04 100644 --- a/src/lib/utils/helpers.ts +++ b/src/lib/utils/helpers.ts @@ -4,3 +4,13 @@ import { clsx, type ClassValue } from "clsx"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +export function replaceTemplateValues(obj: object, values: object) { + const newObj = { ...obj }; + Object.keys(newObj).forEach((key) => { + if (typeof newObj[key] === "string") { + newObj[key] = obj[key].replace(/{{(.*?)}}/g, (_, key) => values[key]); + } + }); + return newObj; +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 8af21ec5..726607f9 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -92,6 +92,6 @@ const config: Config = { }, }, }, - plugins: [], + plugins: [require("@tailwindcss/typography")], }; export default config;