add event request notifications

This commit is contained in:
chark1es 2025-05-29 11:46:46 -07:00
parent 5d92bcfd1b
commit aac2837b78
12 changed files with 1614 additions and 103 deletions

188
bun.lock
View file

@ -3,6 +3,7 @@
"workspaces": {
"": {
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/mdx": "^4.2.3",
"@astrojs/node": "^9.1.3",
"@astrojs/react": "^4.2.3",
@ -38,6 +39,7 @@
"rehype-expressive-code": "^0.40.2",
"resend": "^4.5.1",
"tailwindcss": "^3.4.16",
"typescript": "^5.8.3",
},
"devDependencies": {
"@types/prismjs": "^1.26.5",
@ -58,10 +60,14 @@
"@antfu/utils": ["@antfu/utils@0.7.10", "", {}, "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww=="],
"@astrojs/check": ["@astrojs/check@0.9.4", "", { "dependencies": { "@astrojs/language-server": "^2.15.0", "chokidar": "^4.0.1", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "astro-check": "dist/bin.js" } }, "sha512-IOheHwCtpUfvogHHsvu0AbeRZEnjJg3MopdLddkJE70mULItS/Vh37BHcI00mcOJcH1vhD3odbpvWokpxam7xA=="],
"@astrojs/compiler": ["@astrojs/compiler@2.10.3", "", {}, "sha512-bL/O7YBxsFt55YHU021oL+xz+B/9HvGNId3F9xURN16aeqDK9juHGktdkCSXz+U4nqFACq6ZFvWomOzhV+zfPw=="],
"@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
"@astrojs/language-server": ["@astrojs/language-server@2.15.4", "", { "dependencies": { "@astrojs/compiler": "^2.10.3", "@astrojs/yaml2ts": "^0.2.2", "@jridgewell/sourcemap-codec": "^1.4.15", "@volar/kit": "~2.4.7", "@volar/language-core": "~2.4.7", "@volar/language-server": "~2.4.7", "@volar/language-service": "~2.4.7", "fast-glob": "^3.2.12", "muggle-string": "^0.4.1", "volar-service-css": "0.0.62", "volar-service-emmet": "0.0.62", "volar-service-html": "0.0.62", "volar-service-prettier": "0.0.62", "volar-service-typescript": "0.0.62", "volar-service-typescript-twoslash-queries": "0.0.62", "volar-service-yaml": "0.0.62", "vscode-html-languageservice": "^5.2.0", "vscode-uri": "^3.0.8" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier", "prettier-plugin-astro"], "bin": { "astro-ls": "bin/nodeServer.js" } }, "sha512-JivzASqTPR2bao9BWsSc/woPHH7OGSGc9aMxXL4U6egVTqBycB3ZHdBJPuOCVtcGLrzdWTosAqVPz1BVoxE0+A=="],
"@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.1", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.2.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.1", "remark-smartypants": "^3.0.2", "shiki": "^3.0.0", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-c5F5gGrkczUaTVgmMW9g1YMJGzOtRvjjhw6IfGuxarM6ct09MpwysP10US729dy07gg8y+ofVifezvP3BNsWZg=="],
"@astrojs/mdx": ["@astrojs/mdx@4.2.3", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.1", "@mdx-js/mdx": "^3.1.0", "acorn": "^8.14.1", "es-module-lexer": "^1.6.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "kleur": "^4.1.5", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.4", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-oteB88udzzZmix5kWWUMeMJfeB2Dj8g7jy9LVNuTzGlBh3mEkGhQr6FsIR43p0JKCN11fl5J7P/Ev4Q0Nf0KQQ=="],
@ -76,6 +82,8 @@
"@astrojs/telemetry": ["@astrojs/telemetry@3.2.0", "", { "dependencies": { "ci-info": "^4.1.0", "debug": "^4.3.7", "dlv": "^1.1.3", "dset": "^3.1.4", "is-docker": "^3.0.0", "is-wsl": "^3.1.0", "which-pm-runs": "^1.1.0" } }, "sha512-wxhSKRfKugLwLlr4OFfcqovk+LIFtKwLyGPqMsv+9/ibqqnW3Gv7tBhtKEb0gAyUAC4G9BTVQeQahqnQAhd6IQ=="],
"@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.2", "", { "dependencies": { "yaml": "^2.5.0" } }, "sha512-GOfvSr5Nqy2z5XiwqTouBBpy5FyI6DEe+/g/Mk5am9SjILN1S5fOEvYK0GuWHg98yS/dobP4m8qyqw/URW35fQ=="],
"@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
"@babel/compat-data": ["@babel/compat-data@7.26.2", "", {}, "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg=="],
@ -116,6 +124,20 @@
"@ctrl/tinycolor": ["@ctrl/tinycolor@4.1.0", "", {}, "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ=="],
"@emmetio/abbreviation": ["@emmetio/abbreviation@2.3.3", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA=="],
"@emmetio/css-abbreviation": ["@emmetio/css-abbreviation@2.1.8", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw=="],
"@emmetio/css-parser": ["@emmetio/css-parser@0.4.0", "", { "dependencies": { "@emmetio/stream-reader": "^2.2.0", "@emmetio/stream-reader-utils": "^0.1.0" } }, "sha512-z7wkxRSZgrQHXVzObGkXG+Vmj3uRlpM11oCZ9pbaz0nFejvCDmAiNDpY75+wgXOcffKpj4rzGtwGaZxfJKsJxw=="],
"@emmetio/html-matcher": ["@emmetio/html-matcher@1.3.0", "", { "dependencies": { "@emmetio/scanner": "^1.0.0" } }, "sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ=="],
"@emmetio/scanner": ["@emmetio/scanner@1.0.4", "", {}, "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA=="],
"@emmetio/stream-reader": ["@emmetio/stream-reader@2.2.0", "", {}, "sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw=="],
"@emmetio/stream-reader-utils": ["@emmetio/stream-reader-utils@0.1.0", "", {}, "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A=="],
"@emnapi/runtime": ["@emnapi/runtime@1.3.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag=="],
@ -732,13 +754,31 @@
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.3.4", "", { "dependencies": { "@babel/core": "^7.26.0", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.14.2" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug=="],
"@volar/kit": ["@volar/kit@2.4.14", "", { "dependencies": { "@volar/language-service": "2.4.14", "@volar/typescript": "2.4.14", "typesafe-path": "^0.2.2", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "typescript": "*" } }, "sha512-kBcmHjEodtmYGJELHePZd2JdeYm4ZGOd9F/pQ1YETYIzAwy4Z491EkJ1nRSo/GTxwKt0XYwYA/dHSEgXecVHRA=="],
"@volar/language-core": ["@volar/language-core@2.4.14", "", { "dependencies": { "@volar/source-map": "2.4.14" } }, "sha512-X6beusV0DvuVseaOEy7GoagS4rYHgDHnTrdOj5jeUb49fW5ceQyP9Ej5rBhqgz2wJggl+2fDbbojq1XKaxDi6w=="],
"@volar/language-server": ["@volar/language-server@2.4.14", "", { "dependencies": { "@volar/language-core": "2.4.14", "@volar/language-service": "2.4.14", "@volar/typescript": "2.4.14", "path-browserify": "^1.0.1", "request-light": "^0.7.0", "vscode-languageserver": "^9.0.1", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-P3mGbQbW0v40UYBnb3DAaNtRYx6/MGOVKzdOWmBCGwjUkCR2xBkGrCFt05XnPDwFS/cTWDh2U6Mc9lpZ8Aecfw=="],
"@volar/language-service": ["@volar/language-service@2.4.14", "", { "dependencies": { "@volar/language-core": "2.4.14", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-vNC3823EJohdzLTyjZoCMPwoWCfINB5emusniCkW5CGoGHQov4VVmT6yI5ncgP/NpgAIUv2NEkJooXvLHA4VeQ=="],
"@volar/source-map": ["@volar/source-map@2.4.14", "", {}, "sha512-5TeKKMh7Sfxo8021cJfmBzcjfY1SsXsPMMjMvjY7ivesdnybqqS+GxGAoXHAOUawQTwtdUxgP65Im+dEmvWtYQ=="],
"@volar/typescript": ["@volar/typescript@2.4.14", "", { "dependencies": { "@volar/language-core": "2.4.14", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-p8Z6f/bZM3/HyCdRNFZOEEzts51uV8WHeN8Tnfnm2EBv6FDB2TQLzfVx7aJvnl8ofKAOnS64B2O8bImBFaauRw=="],
"@vscode/emmet-helper": ["@vscode/emmet-helper@2.11.0", "", { "dependencies": { "emmet": "^2.4.3", "jsonc-parser": "^2.3.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.15.1", "vscode-uri": "^3.0.8" } }, "sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw=="],
"@vscode/l10n": ["@vscode/l10n@0.0.18", "", {}, "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ=="],
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
"ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
@ -818,7 +858,7 @@
"cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="],
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
@ -828,6 +868,8 @@
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
@ -940,7 +982,9 @@
"electron-to-chromium": ["electron-to-chromium@1.5.134", "", {}, "sha512-zSwzrLg3jNP3bwsLqWHmS5z2nIOQ5ngMnfMZOWWtXnqqQkPVyOipxK98w+1beLw1TB+EImPNcG8wVP/cLVs2Og=="],
"emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
"emmet": ["emmet@2.4.11", "", { "dependencies": { "@emmetio/abbreviation": "^2.3.3", "@emmetio/css-abbreviation": "^2.1.8" } }, "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="],
@ -994,6 +1038,8 @@
"fast-glob": ["fast-glob@3.3.2", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow=="],
"fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="],
"fastparse": ["fastparse@1.1.2", "", {}, "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ=="],
"fastq": ["fastq@1.17.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w=="],
@ -1028,6 +1074,8 @@
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="],
"get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
@ -1150,8 +1198,12 @@
"jsesc": ["jsesc@3.0.2", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="],
"jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
@ -1316,6 +1368,8 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
"nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
@ -1376,6 +1430,8 @@
"parseley": ["parseley@0.12.1", "", { "dependencies": { "leac": "^0.6.0", "peberminta": "^0.9.0" } }, "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw=="],
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
@ -1456,7 +1512,7 @@
"readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
@ -1498,6 +1554,12 @@
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
"request-light": ["request-light@0.7.0", "", {}, "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"resend": ["resend@4.5.1", "", { "dependencies": { "@react-email/render": "1.0.6" } }, "sha512-ryhHpZqCBmuVyzM19IO8Egtc2hkWI4JOL5lf5F3P7Dydu3rFeX6lHNpGqG0tjWoZ63rw0l731JEmuJZBdDm3og=="],
"resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="],
@ -1566,7 +1628,7 @@
"streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="],
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@ -1574,7 +1636,7 @@
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
"strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@ -1626,7 +1688,11 @@
"type-fest": ["type-fest@4.30.0", "", {}, "sha512-G6zXWS1dLj6eagy6sVhOMQiLtJdxQBHIA9Z6HFUNLOlr6MFOgzV8wvmidtPONfPtEUv0uZsy77XJNzTAfwPDaA=="],
"typescript": ["typescript@5.7.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg=="],
"typesafe-path": ["typesafe-path@0.2.2", "", {}, "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"typescript-auto-import-cache": ["typescript-auto-import-cache@0.3.6", "", { "dependencies": { "semver": "^7.3.8" } }, "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ=="],
"ufo": ["ufo@1.5.4", "", {}, "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="],
@ -1684,6 +1750,40 @@
"vitefu": ["vitefu@1.0.6", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA=="],
"volar-service-css": ["volar-service-css@0.0.62", "", { "dependencies": { "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-JwNyKsH3F8PuzZYuqPf+2e+4CTU8YoyUHEHVnoXNlrLe7wy9U3biomZ56llN69Ris7TTy/+DEX41yVxQpM4qvg=="],
"volar-service-emmet": ["volar-service-emmet@0.0.62", "", { "dependencies": { "@emmetio/css-parser": "^0.4.0", "@emmetio/html-matcher": "^1.3.0", "@vscode/emmet-helper": "^2.9.3", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-U4dxWDBWz7Pi4plpbXf4J4Z/ss6kBO3TYrACxWNsE29abu75QzVS0paxDDhI6bhqpbDFXlpsDhZ9aXVFpnfGRQ=="],
"volar-service-html": ["volar-service-html@0.0.62", "", { "dependencies": { "vscode-html-languageservice": "^5.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-Zw01aJsZRh4GTGUjveyfEzEqpULQUdQH79KNEiKVYHZyuGtdBRYCHlrus1sueSNMxwwkuF5WnOHfvBzafs8yyQ=="],
"volar-service-prettier": ["volar-service-prettier@0.0.62", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0", "prettier": "^2.2 || ^3.0" }, "optionalPeers": ["@volar/language-service", "prettier"] }, "sha512-h2yk1RqRTE+vkYZaI9KYuwpDfOQRrTEMvoHol0yW4GFKc75wWQRrb5n/5abDrzMPrkQbSip8JH2AXbvrRtYh4w=="],
"volar-service-typescript": ["volar-service-typescript@0.0.62", "", { "dependencies": { "path-browserify": "^1.0.1", "semver": "^7.6.2", "typescript-auto-import-cache": "^0.3.3", "vscode-languageserver-textdocument": "^1.0.11", "vscode-nls": "^5.2.0", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-p7MPi71q7KOsH0eAbZwPBiKPp9B2+qrdHAd6VY5oTo9BUXatsOAdakTm9Yf0DUj6uWBAaOT01BSeVOPwucMV1g=="],
"volar-service-typescript-twoslash-queries": ["volar-service-typescript-twoslash-queries@0.0.62", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-KxFt4zydyJYYI0kFAcWPTh4u0Ha36TASPZkAnNY784GtgajerUqM80nX/W1d0wVhmcOFfAxkVsf/Ed+tiYU7ng=="],
"volar-service-yaml": ["volar-service-yaml@0.0.62", "", { "dependencies": { "vscode-uri": "^3.0.8", "yaml-language-server": "~1.15.0" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-k7gvv7sk3wa+nGll3MaSKyjwQsJjIGCHFjVkl3wjaSP2nouKyn9aokGmqjrl39mi88Oy49giog2GkZH526wjig=="],
"vscode-css-languageservice": ["vscode-css-languageservice@6.3.5", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-ehEIMXYPYEz/5Svi2raL9OKLpBt5dSAdoCFoLpo0TVFKrVpDemyuQwS3c3D552z/qQCg3pMp8oOLMObY6M3ajQ=="],
"vscode-html-languageservice": ["vscode-html-languageservice@5.4.0", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-9/cbc90BSYCghmHI7/VbWettHZdC7WYpz2g5gBK6UDUI1MkZbM773Q12uAYJx9jzAiNHPpyo6KzcwmcnugncAQ=="],
"vscode-json-languageservice": ["vscode-json-languageservice@4.1.8", "", { "dependencies": { "jsonc-parser": "^3.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-nls": "^5.0.0", "vscode-uri": "^3.0.2" } }, "sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg=="],
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
"vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="],
"vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="],
"vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="],
"vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
"vscode-nls": ["vscode-nls@5.2.0", "", {}, "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng=="],
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
"whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
@ -1704,10 +1804,16 @@
"xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"yaml": ["yaml@2.6.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg=="],
"yaml-language-server": ["yaml-language-server@1.15.0", "", { "dependencies": { "ajv": "^8.11.0", "lodash": "4.17.21", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", "vscode-languageserver": "^7.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-nls": "^5.0.0", "vscode-uri": "^3.0.2", "yaml": "2.2.2" }, "optionalDependencies": { "prettier": "2.8.7" }, "bin": { "yaml-language-server": "bin/yaml-language-server" } }, "sha512-N47AqBDCMQmh6mBLmI6oqxryHRzi33aPFPsJhYy3VTUGCdLHYjGh4FZzpUjRlphaADBBkDmnkM/++KNIOHi5Rw=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
@ -1730,6 +1836,8 @@
"@antfu/install-pkg/tinyexec": ["tinyexec@0.3.1", "", {}, "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ=="],
"@astrojs/language-server/@astrojs/compiler": ["@astrojs/compiler@2.11.0", "", {}, "sha512-zZOO7i+JhojO8qmlyR/URui6LyfHJY6m+L9nwyX5GiKD78YoRaZ5tzz6X0fkl+5bD3uwlDHayf6Oe8Fu36RKNg=="],
"@astrojs/telemetry/ci-info": ["ci-info@4.1.0", "", {}, "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A=="],
"@astrojs/telemetry/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
@ -1762,6 +1870,8 @@
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
"@react-aria/calendar/@react-aria/i18n": ["@react-aria/i18n@3.12.7", "", { "dependencies": { "@internationalized/date": "^3.7.0", "@internationalized/message": "^3.1.6", "@internationalized/number": "^3.6.0", "@internationalized/string": "^3.2.5", "@react-aria/ssr": "^3.9.7", "@react-aria/utils": "^3.28.1", "@react-types/shared": "^3.28.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-eLbYO2xrpeOKIEmLv2KD5LFcB0wltFqS+pUjsOzkKZg6H3b6AFDmJPxr/a0x2KGHtpGJvuHwCSbpPi9PzSSQLg=="],
@ -1852,7 +1962,7 @@
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"ajv/fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
@ -1862,9 +1972,11 @@
"autoprefixer/caniuse-lite": ["caniuse-lite@1.0.30001712", "", {}, "sha512-MBqPpGYYdQ7/hfKiet9SCI+nmN5/hp4ZzveOJubl5DTAMa5oggjAuoi0Z4onBpKPFI2ePGnQuQIzF3VxDjDJig=="],
"boxen/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"browserslist/caniuse-lite": ["caniuse-lite@1.0.30001712", "", {}, "sha512-MBqPpGYYdQ7/hfKiet9SCI+nmN5/hp4ZzveOJubl5DTAMa5oggjAuoi0Z4onBpKPFI2ePGnQuQIzF3VxDjDJig=="],
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
@ -1916,8 +2028,6 @@
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"rehype-stringify/hast-util-to-html": ["hast-util-to-html@9.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg=="],
"rollup/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
@ -1926,25 +2036,31 @@
"sharp/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"svgo/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
"tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"tailwindcss/postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="],
"tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
"unstorage/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"vscode-json-languageservice/jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
"widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"yaml-language-server/prettier": ["prettier@2.8.7", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw=="],
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="],
"yaml-language-server/vscode-languageserver": ["vscode-languageserver@7.0.0", "", { "dependencies": { "vscode-languageserver-protocol": "3.16.0" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw=="],
"yaml-language-server/yaml": ["yaml@2.2.2", "", {}, "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA=="],
"@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.68", "", {}, "sha512-FgMdJlma0OzUYlbrtZ4AeXjKxKPk6KT8WOP8BjcqxWtlg8qyJQjRzPJzUtUn5GBg1oQ26hFs7HOOHJMYiJRnvQ=="],
@ -1972,6 +2088,8 @@
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"@react-aria/combobox/@react-aria/selection/@react-aria/focus": ["@react-aria/focus@3.20.1", "", { "dependencies": { "@react-aria/interactions": "^3.24.1", "@react-aria/utils": "^3.28.1", "@react-types/shared": "^3.28.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-lgYs+sQ1TtBrAXnAdRBQrBo0/7o5H6IrfDxec1j+VRpcXL0xyk0xPq+m3lZp8typzIghqDgpnKkJ5Jf4OrzPIw=="],
"@react-aria/combobox/@react-aria/selection/@react-aria/interactions": ["@react-aria/interactions@3.24.1", "", { "dependencies": { "@react-aria/ssr": "^3.9.7", "@react-aria/utils": "^3.28.1", "@react-stately/flags": "^3.1.0", "@react-types/shared": "^3.28.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-OWEcIC6UQfWq4Td5Ptuh4PZQ4LHLJr/JL2jGYvuNL6EgL3bWvzPrRYIF/R64YbfVxIC7FeZpPSkS07sZ93/NoA=="],
@ -1994,9 +2112,11 @@
"@react-aria/tabs/@react-aria/selection/@react-aria/interactions": ["@react-aria/interactions@3.24.1", "", { "dependencies": { "@react-aria/ssr": "^3.9.7", "@react-aria/utils": "^3.28.1", "@react-stately/flags": "^3.1.0", "@react-types/shared": "^3.28.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-OWEcIC6UQfWq4Td5Ptuh4PZQ4LHLJr/JL2jGYvuNL6EgL3bWvzPrRYIF/R64YbfVxIC7FeZpPSkS07sZ93/NoA=="],
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"boxen/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"boxen/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
@ -2008,19 +2128,33 @@
"rehype-stringify/hast-util-to-html/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="],
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"unstorage/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"widest-line/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"widest-line/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"yaml-language-server/vscode-languageserver/vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.16.0", "", { "dependencies": { "vscode-jsonrpc": "6.0.0", "vscode-languageserver-types": "3.16.0" } }, "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A=="],
"@expressive-code/plugin-shiki/shiki/@shikijs/core/hast-util-to-html": ["hast-util-to-html@9.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA=="],
"@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"boxen/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"widest-line/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"yaml-language-server/vscode-languageserver/vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@6.0.0", "", {}, "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg=="],
"yaml-language-server/vscode-languageserver/vscode-languageserver-protocol/vscode-languageserver-types": ["vscode-languageserver-types@3.16.0", "", {}, "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA=="],
"@expressive-code/plugin-shiki/shiki/@shikijs/core/hast-util-to-html/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="],

View file

@ -13,6 +13,7 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/mdx": "^4.2.3",
"@astrojs/node": "^9.1.3",
"@astrojs/react": "^4.2.3",
@ -47,7 +48,8 @@
"react-icons": "^5.4.0",
"rehype-expressive-code": "^0.40.2",
"resend": "^4.5.1",
"tailwindcss": "^3.4.16"
"tailwindcss": "^3.4.16",
"typescript": "^5.8.3"
},
"devDependencies": {
"@types/prismjs": "^1.26.5",

View file

@ -7,6 +7,7 @@ import { FileManager } from '../../../scripts/pocketbase/FileManager';
import { DataSyncService } from '../../../scripts/database/DataSyncService';
import { Collections } from '../../../schemas/pocketbase/schema';
import { EventRequestStatus } from '../../../schemas/pocketbase';
import { EmailClient } from '../../../scripts/email/EmailClient';
// Form sections
import PRSection from './PRSection';
@ -88,7 +89,6 @@ import CustomAlert from '../universal/CustomAlert';
const EventRequestForm: React.FC = () => {
const [currentStep, setCurrentStep] = useState<number>(1);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
// Initialize form data
const [formData, setFormData] = useState<EventRequestFormData>({
@ -255,7 +255,6 @@ const EventRequestForm: React.FC = () => {
}
setIsSubmitting(true);
setError(null);
try {
const auth = Authentication.getInstance();
@ -359,33 +358,100 @@ const EventRequestForm: React.FC = () => {
// Force sync the event requests collection to update IndexedDB
await dataSync.syncCollection(Collections.EVENT_REQUESTS);
// Upload files if they exist
console.log('Event request record created:', record.id);
// Upload files if they exist - handle each file type separately
const fileUploadErrors: string[] = [];
// Upload other logos
if (formData.other_logos.length > 0) {
await fileManager.uploadFiles('event_request', record.id, 'other_logos', formData.other_logos);
try {
console.log('Uploading other logos:', formData.other_logos.length, 'files');
console.log('Other logos files:', formData.other_logos.map(f => ({ name: f.name, size: f.size, type: f.type })));
await fileManager.uploadFiles('event_request', record.id, 'other_logos', formData.other_logos);
console.log('Other logos uploaded successfully');
} catch (error) {
console.error('Failed to upload other logos:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
fileUploadErrors.push(`Failed to upload custom logo files: ${errorMessage}`);
}
}
// Upload room booking
if (formData.room_booking) {
await fileManager.uploadFile('event_request', record.id, 'room_booking', formData.room_booking);
try {
console.log('Uploading room booking file:', { name: formData.room_booking.name, size: formData.room_booking.size, type: formData.room_booking.type });
await fileManager.uploadFile('event_request', record.id, 'room_booking', formData.room_booking);
console.log('Room booking file uploaded successfully');
} catch (error) {
console.error('Failed to upload room booking:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
fileUploadErrors.push(`Failed to upload room booking file: ${errorMessage}`);
}
}
// Upload multiple invoice files
// Upload invoice files
if (formData.invoice_files && formData.invoice_files.length > 0) {
await fileManager.appendFiles('event_request', record.id, 'invoice_files', formData.invoice_files);
try {
console.log('Uploading invoice files:', formData.invoice_files.length, 'files');
console.log('Invoice files:', formData.invoice_files.map(f => ({ name: f.name, size: f.size, type: f.type })));
await fileManager.appendFiles('event_request', record.id, 'invoice_files', formData.invoice_files);
// For backward compatibility, also upload the first file as the main invoice
if (formData.invoice || formData.invoice_files[0]) {
const mainInvoice = formData.invoice || formData.invoice_files[0];
await fileManager.uploadFile('event_request', record.id, 'invoice', mainInvoice);
// For backward compatibility, also upload the first file as the main invoice
if (formData.invoice || formData.invoice_files[0]) {
const mainInvoice = formData.invoice || formData.invoice_files[0];
console.log('Uploading main invoice file:', { name: mainInvoice.name, size: mainInvoice.size, type: mainInvoice.type });
await fileManager.uploadFile('event_request', record.id, 'invoice', mainInvoice);
}
console.log('Invoice files uploaded successfully');
} catch (error) {
console.error('Failed to upload invoice files:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
fileUploadErrors.push(`Failed to upload invoice files: ${errorMessage}`);
}
} else if (formData.invoice) {
await fileManager.uploadFile('event_request', record.id, 'invoice', formData.invoice);
try {
console.log('Uploading single invoice file:', { name: formData.invoice.name, size: formData.invoice.size, type: formData.invoice.type });
await fileManager.uploadFile('event_request', record.id, 'invoice', formData.invoice);
console.log('Invoice file uploaded successfully');
} catch (error) {
console.error('Failed to upload invoice file:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
fileUploadErrors.push(`Failed to upload invoice file: ${errorMessage}`);
}
}
// Show file upload warnings if any occurred
if (fileUploadErrors.length > 0) {
console.warn('File upload errors:', fileUploadErrors);
// Show each file upload error as a separate toast for better UX
fileUploadErrors.forEach(error => {
toast.error(error, {
duration: 6000, // Longer duration for file upload errors
position: 'top-right'
});
});
// Also show a summary toast
toast.error(`Event request submitted successfully, but ${fileUploadErrors.length} file upload(s) failed. Please check the errors above and re-upload the files manually.`, {
duration: 8000,
position: 'top-center'
});
} else {
// Keep success toast for form submission since it's a user action
toast.success('Event request submitted successfully!');
}
// Clear form data from localStorage
localStorage.removeItem('eventRequestFormData');
// Keep success toast for form submission since it's a user action
toast.success('Event request submitted successfully!');
// Send email notification to coordinators (non-blocking)
try {
await EmailClient.notifyEventRequestSubmission(record.id);
console.log('Event request notification email sent successfully');
} catch (emailError) {
console.error('Failed to send event request notification email:', emailError);
// Don't show error to user - email failure shouldn't disrupt the main flow
}
// Reset form
resetForm();
@ -398,7 +464,6 @@ const EventRequestForm: React.FC = () => {
} catch (error) {
console.error('Error submitting event request:', error);
toast.error('Failed to submit event request. Please try again.');
setError('Failed to submit event request. Please try again.');
} finally {
setIsSubmitting(false);
}
@ -515,7 +580,8 @@ const EventRequestForm: React.FC = () => {
}
if (errors.length > 0) {
setError(errors[0]);
// Show the first error as a toast instead of setting error state
toast.error(errors[0]);
return false;
}
@ -560,7 +626,7 @@ const EventRequestForm: React.FC = () => {
if (formData.as_funding_required) {
// Check if invoice data is present and has items
if (!formData.invoiceData || !formData.invoiceData.items || formData.invoiceData.items.length === 0) {
setError('Please add at least one item to your invoice');
toast.error('Please add at least one item to your invoice');
return false;
}
@ -572,7 +638,7 @@ const EventRequestForm: React.FC = () => {
// Check if the budget exceeds the maximum allowed ($5000 cap regardless of attendance)
const maxBudget = Math.min(formData.expected_attendance * 10, 5000);
if (totalBudget > maxBudget) {
setError(`Your budget (${totalBudget.toFixed(2)} dollars) exceeds the maximum allowed (${maxBudget} dollars). The absolute maximum is $5,000.`);
toast.error(`Your budget ($${totalBudget.toFixed(2)}) exceeds the maximum allowed ($${maxBudget}). The absolute maximum is $5,000.`);
return false;
}
}
@ -903,21 +969,6 @@ const EventRequestForm: React.FC = () => {
}}
className="space-y-6"
>
{error && (
<motion.div
initial={{ opacity: 0, y: -20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.95 }}
>
<CustomAlert
type="error"
title="Error"
message={error}
icon="heroicons:exclamation-triangle"
/>
</motion.div>
)}
{/* Progress indicator */}
<div className="w-full mb-6">
<div className="flex justify-between mb-2">

View file

@ -32,6 +32,7 @@ interface ExtendedEventRequest extends SchemaEventRequest {
room_booking?: string; // Single file for room booking
room_reservation_needed?: boolean; // Keep for backward compatibility
additional_notes?: string;
flyers_completed?: boolean; // Track if flyers have been completed by PR team
}
interface EventRequestDetailsProps {
@ -623,7 +624,7 @@ const ASFundingTab: React.FC<{ request: ExtendedEventRequest }> = ({ request })
) : (
<Icon icon="mdi:file-document" className="h-8 w-8 text-secondary" />
)}
<div className="flex-grow">
<div className="flex-1 min-w-0">
<p className="font-medium truncate" title={fileId}>
{displayName}
</p>
@ -1078,6 +1079,8 @@ const InvoiceTable: React.FC<{ invoiceData: any, expectedAttendance?: number }>
const PRMaterialsTab: React.FC<{ request: ExtendedEventRequest }> = ({ request }) => {
const [isPreviewModalOpen, setIsPreviewModalOpen] = useState<boolean>(false);
const [selectedFile, setSelectedFile] = useState<{ name: string, displayName: string }>({ name: '', displayName: '' });
const [flyersCompleted, setFlyersCompleted] = useState<boolean>(request.flyers_completed || false);
const [isUpdating, setIsUpdating] = useState<boolean>(false);
// Format date for display
const formatDate = (dateString: string) => {
@ -1096,6 +1099,30 @@ const PRMaterialsTab: React.FC<{ request: ExtendedEventRequest }> = ({ request }
}
};
// Handle flyers completed checkbox change
const handleFlyersCompletedChange = async (completed: boolean) => {
setIsUpdating(true);
try {
const { Update } = await import('../../../scripts/pocketbase/Update');
const update = Update.getInstance();
await update.updateField("event_request", request.id, "flyers_completed", completed);
setFlyersCompleted(completed);
toast.success(`Flyers completion status updated to ${completed ? 'completed' : 'not completed'}`);
} catch (error) {
console.error('Failed to update flyers completed status:', error);
toast.error('Failed to update flyers completion status');
} finally {
setIsUpdating(false);
}
};
// Sync local state with request prop changes
useEffect(() => {
setFlyersCompleted(request.flyers_completed || false);
}, [request.flyers_completed]);
// Use the same utility functions as in the ASFundingTab
const getFileExtension = (filename: string): string => {
const parts = filename.split('.');
@ -1158,6 +1185,46 @@ const PRMaterialsTab: React.FC<{ request: ExtendedEventRequest }> = ({ request }
</div>
</div>
{/* Flyers Completed Checkbox - Only show if flyers are needed */}
{request.flyers_needed && (
<motion.div
className="bg-base-300/20 p-4 rounded-lg"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.05 }}
>
<h4 className="text-sm font-medium text-gray-400 mb-3">Completion Status</h4>
<div className="flex items-center gap-3">
<input
type="checkbox"
checked={flyersCompleted}
onChange={(e) => handleFlyersCompletedChange(e.target.checked)}
disabled={isUpdating}
className="checkbox checkbox-primary"
/>
<label className="text-sm font-medium">
Flyers completed by PR team
</label>
{isUpdating && (
<div className="loading loading-spinner loading-sm"></div>
)}
</div>
<div className="mt-2">
{flyersCompleted ? (
<span className="badge badge-success gap-1">
<Icon icon="mdi:check-circle" className="h-3 w-3" />
Completed
</span>
) : (
<span className="badge badge-warning gap-1">
<Icon icon="mdi:clock" className="h-3 w-3" />
Pending
</span>
)}
</div>
</motion.div>
)}
{request.flyers_needed && (
<motion.div
className="space-y-4"
@ -1435,6 +1502,9 @@ const EventRequestDetails = ({
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false);
const [newStatus, setNewStatus] = useState<"submitted" | "pending" | "completed" | "declined">("pending");
const [isSubmitting, setIsSubmitting] = useState(false);
// Add state for decline reason modal
const [isDeclineModalOpen, setIsDeclineModalOpen] = useState(false);
const [declineReason, setDeclineReason] = useState<string>('');
const [alertInfo, setAlertInfo] = useState<{ show: boolean; type: "success" | "error" | "warning" | "info"; message: string }>({
show: false,
type: "info",
@ -1465,8 +1535,14 @@ const EventRequestDetails = ({
};
const handleStatusChange = async (newStatus: "submitted" | "pending" | "completed" | "declined") => {
setNewStatus(newStatus);
setIsConfirmModalOpen(true);
if (newStatus === 'declined') {
// Open decline reason modal instead of immediate confirmation
setDeclineReason('');
setIsDeclineModalOpen(true);
} else {
setNewStatus(newStatus);
setIsConfirmModalOpen(true);
}
};
const confirmStatusChange = async () => {
@ -1492,6 +1568,72 @@ const EventRequestDetails = ({
}
};
// Handle decline with reason
const handleDeclineWithReason = async () => {
if (!declineReason.trim()) {
toast.error('Please provide a reason for declining');
return;
}
setIsSubmitting(true);
try {
// Use Update service to update both status and decline reason
const { Update } = await import('../../../scripts/pocketbase/Update');
const update = Update.getInstance();
await update.updateFields("event_request", request.id, {
status: 'declined',
declined_reason: declineReason
});
// Send email notifications
const { EmailClient } = await import('../../../scripts/email/EmailClient');
const auth = Authentication.getInstance();
const changedByUserId = auth.getUserId();
await EmailClient.notifyEventRequestStatusChange(
request.id,
request.status,
'declined',
changedByUserId || undefined,
declineReason
);
// Send design team notification if PR materials were needed
if (request.flyers_needed) {
await EmailClient.notifyDesignTeam(request.id, 'declined');
}
setAlertInfo({
show: true,
type: "success",
message: "Event request has been declined successfully."
});
setIsDeclineModalOpen(false);
setDeclineReason('');
// Call the parent's onStatusChange if needed for UI updates
await onStatusChange(request.id, 'declined');
} catch (error) {
console.error('Error declining request:', error);
setAlertInfo({
show: true,
type: "error",
message: "Failed to decline event request. Please try again."
});
} finally {
setIsSubmitting(false);
}
};
// Cancel decline action
const cancelDecline = () => {
setIsDeclineModalOpen(false);
setDeclineReason('');
};
return (
<div className="bg-transparent w-full">
{/* Tabs navigation */}
@ -1809,6 +1951,56 @@ const EventRequestDetails = ({
</div>
)}
{/* Decline Reason Modal */}
{isDeclineModalOpen && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-[300] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="bg-base-300 rounded-lg p-6 w-full max-w-md"
>
<h3 className="text-lg font-bold mb-4">Decline Event Request</h3>
<p className="text-gray-300 mb-4">
Please provide a reason for declining "{request.name}". This will be sent to the submitter and they will need to resubmit with proper information.
</p>
<textarea
className="textarea textarea-bordered w-full h-32 bg-base-100 text-white border-base-300 focus:border-primary"
placeholder="Enter decline reason (required)..."
value={declineReason}
onChange={(e) => setDeclineReason(e.target.value)}
maxLength={500}
/>
<div className="text-xs text-gray-400 mb-4">
{declineReason.length}/500 characters
</div>
<div className="flex justify-end gap-3">
<button
className="btn btn-ghost"
onClick={cancelDecline}
disabled={isSubmitting}
>
Cancel
</button>
<button
className="btn btn-error"
onClick={handleDeclineWithReason}
disabled={!declineReason.trim() || isSubmitting}
>
{isSubmitting ? (
<>
<span className="loading loading-spinner loading-xs"></span>
Declining...
</>
) : (
'Decline Request'
)}
</button>
</div>
</motion.div>
</div>
)}
{/* File Preview Modal */}
<dialog id="file-preview-modal" className="modal modal-bottom sm:modal-middle">
<div className="modal-box bg-base-200 p-0 overflow-hidden max-w-4xl">

View file

@ -24,6 +24,8 @@ interface ExtendedEventRequest extends SchemaEventRequest {
invoice_data?: any;
invoice_files?: string[]; // Array of invoice file IDs
status: "submitted" | "pending" | "completed" | "declined";
declined_reason?: string; // Reason for declining the event request
flyers_completed?: boolean; // Track if flyers have been completed by PR team
}
interface EventRequestManagementTableProps {
@ -50,6 +52,10 @@ const EventRequestManagementTable = ({
// Add state for update modal
const [isUpdateModalOpen, setIsUpdateModalOpen] = useState<boolean>(false);
const [requestToUpdate, setRequestToUpdate] = useState<ExtendedEventRequest | null>(null);
// Add state for decline reason modal
const [isDeclineModalOpen, setIsDeclineModalOpen] = useState<boolean>(false);
const [declineReason, setDeclineReason] = useState<string>('');
const [requestToDecline, setRequestToDecline] = useState<ExtendedEventRequest | null>(null);
// Refresh event requests
const refreshEventRequests = async () => {
@ -177,40 +183,125 @@ const EventRequestManagementTable = ({
};
// Update event request status
const updateEventRequestStatus = async (id: string, status: "submitted" | "pending" | "completed" | "declined"): Promise<void> => {
const updateEventRequestStatus = async (id: string, status: "submitted" | "pending" | "completed" | "declined", declineReason?: string): Promise<void> => {
try {
await onStatusChange(id, status);
// Find the event request to get its current status and name
const eventRequest = eventRequests.find(req => req.id === id);
const eventName = eventRequest?.name || 'Event';
const previousStatus = eventRequest?.status;
// Find the event request to get its name
// If declining, update with decline reason
if (status === 'declined' && declineReason) {
const { Update } = await import('../../../scripts/pocketbase/Update');
const update = Update.getInstance();
await update.updateFields("event_request", id, {
status: status,
declined_reason: declineReason
});
} else {
await onStatusChange(id, status);
}
// Update local state
setEventRequests(prev =>
prev.map(request =>
request.id === id ? {
...request,
status,
...(status === 'declined' && declineReason ? { declined_reason: declineReason } : {})
} : request
)
);
setFilteredRequests(prev =>
prev.map(request =>
request.id === id ? {
...request,
status,
...(status === 'declined' && declineReason ? { declined_reason: declineReason } : {})
} : request
)
);
toast.success(`"${eventName}" status updated to ${status}`);
// Send email notification for status change
try {
const { EmailClient } = await import('../../../scripts/email/EmailClient');
const auth = Authentication.getInstance();
const changedByUserId = auth.getUserId();
if (previousStatus && previousStatus !== status) {
await EmailClient.notifyEventRequestStatusChange(
id,
previousStatus,
status,
changedByUserId || undefined,
status === 'declined' ? declineReason : undefined
);
console.log('Event request status change notification email sent successfully');
}
// Send design team notifications for PR-related actions
if (eventRequest?.flyers_needed) {
if (status === 'declined') {
await EmailClient.notifyDesignTeam(id, 'declined');
console.log('Design team notified of declined PR request');
}
}
} catch (emailError) {
console.error('Failed to send event request status change notification email:', emailError);
// Don't show error to user - email failure shouldn't disrupt the main operation
}
} catch (error) {
console.error('Error updating status:', error);
toast.error('Failed to update status');
}
};
// Update PR status (flyers_completed)
const updatePRStatus = async (id: string, completed: boolean): Promise<void> => {
try {
const { Update } = await import('../../../scripts/pocketbase/Update');
const update = Update.getInstance();
await update.updateField("event_request", id, "flyers_completed", completed);
// Find the event request to get its details
const eventRequest = eventRequests.find(req => req.id === id);
const eventName = eventRequest?.name || 'Event';
// Update local state
setEventRequests(prev =>
prev.map(request =>
request.id === id ? { ...request, status } : request
request.id === id ? { ...request, flyers_completed: completed } : request
)
);
setFilteredRequests(prev =>
prev.map(request =>
request.id === id ? { ...request, status } : request
request.id === id ? { ...request, flyers_completed: completed } : request
)
);
// Force sync to update IndexedDB
await dataSync.syncCollection<ExtendedEventRequest>(Collections.EVENT_REQUESTS);
toast.success(`"${eventName}" PR status updated to ${completed ? 'completed' : 'pending'}`);
// Send email notification if PR is completed
if (completed) {
try {
const { EmailClient } = await import('../../../scripts/email/EmailClient');
await EmailClient.notifyPRCompleted(id);
console.log('PR completion notification email sent successfully');
} catch (emailError) {
console.error('Failed to send PR completion notification email:', emailError);
// Don't show error to user - email failure shouldn't disrupt the main operation
}
}
// Show success toast with event name
toast.success(`"${eventName}" status updated to ${status}`);
} catch (error) {
// Find the event request to get its name
const eventRequest = eventRequests.find(req => req.id === id);
const eventName = eventRequest?.name || 'Event';
// console.error('Error updating status:', error);
toast.error(`Failed to update status for "${eventName}"`);
throw error; // Re-throw the error to be caught by the caller
console.error('Error updating PR status:', error);
toast.error('Failed to update PR status');
}
};
@ -346,6 +437,38 @@ const EventRequestManagementTable = ({
}
};
// Handle decline action with reason prompt
const handleDeclineAction = (request: ExtendedEventRequest) => {
setRequestToDecline(request);
setDeclineReason('');
setIsDeclineModalOpen(true);
};
// Confirm decline with reason
const confirmDecline = async () => {
if (!requestToDecline || !declineReason.trim()) {
toast.error('Please provide a reason for declining');
return;
}
try {
await updateEventRequestStatus(requestToDecline.id, 'declined', declineReason);
setIsDeclineModalOpen(false);
setRequestToDecline(null);
setDeclineReason('');
} catch (error) {
console.error('Error declining request:', error);
toast.error('Failed to decline request');
}
};
// Cancel decline action
const cancelDecline = () => {
setIsDeclineModalOpen(false);
setRequestToDecline(null);
setDeclineReason('');
};
// Apply filters when filter state changes
useEffect(() => {
applyFilters();
@ -576,6 +699,19 @@ const EventRequestManagementTable = ({
</div>
</th>
<th className="hidden lg:table-cell">PR Materials</th>
<th
className="cursor-pointer hover:bg-base-300 transition-colors hidden lg:table-cell"
onClick={() => handleSortChange('flyers_completed')}
>
<div className="flex items-center gap-1">
PR Status
{sortField === 'flyers_completed' && (
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={sortDirection === 'asc' ? "M5 15l7-7 7 7" : "M19 9l-7 7-7-7"} />
</svg>
)}
</div>
</th>
<th className="hidden lg:table-cell">AS Funding</th>
<th
className="cursor-pointer hover:bg-base-300 transition-colors hidden md:table-cell"
@ -637,6 +773,28 @@ const EventRequestManagementTable = ({
<span className="badge badge-ghost badge-sm">No</span>
)}
</td>
<td className="hidden lg:table-cell">
{request.flyers_needed ? (
<input
type="checkbox"
checked={request.flyers_completed || false}
onChange={(e) => {
e.stopPropagation();
updatePRStatus(request.id, e.target.checked);
}}
className="checkbox checkbox-primary"
title="Mark PR materials as completed"
/>
) : (
<input
type="checkbox"
checked={false}
disabled={true}
className="checkbox checkbox-disabled opacity-30"
title="PR materials not needed for this event"
/>
)}
</td>
<td className="hidden lg:table-cell">
{request.as_funding_required ? (
<span className="badge badge-success badge-sm">Yes</span>
@ -671,6 +829,50 @@ const EventRequestManagementTable = ({
</table>
</div>
</motion.div>
{/* Decline Reason Modal */}
{isDeclineModalOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="bg-base-200 rounded-lg p-6 w-full max-w-md shadow-xl"
>
<h3 className="text-lg font-semibold text-white mb-4">
Decline Event Request
</h3>
<p className="text-gray-300 mb-4">
Please provide a reason for declining "{requestToDecline?.name}". This will be sent to the submitter.
</p>
<textarea
className="textarea textarea-bordered w-full h-32 bg-base-300 text-white border-base-300 focus:border-primary"
placeholder="Enter decline reason (required)..."
value={declineReason}
onChange={(e) => setDeclineReason(e.target.value)}
maxLength={500}
/>
<div className="text-xs text-gray-400 mb-4">
{declineReason.length}/500 characters
</div>
<div className="flex justify-end gap-3">
<button
className="btn btn-ghost"
onClick={cancelDecline}
>
Cancel
</button>
<button
className="btn btn-error"
onClick={confirmDecline}
disabled={!declineReason.trim()}
>
Decline Request
</button>
</div>
</motion.div>
</div>
)}
</>
);
};

View file

@ -7,6 +7,7 @@ import { Collections } from '../../../schemas/pocketbase/schema';
import { DataSyncService } from '../../../scripts/database/DataSyncService';
import { Get } from '../../../scripts/pocketbase/Get';
import { toast } from 'react-hot-toast';
import { EmailClient } from '../../../scripts/email/EmailClient';
import type { EventRequest } from '../../../schemas/pocketbase/schema';
// Extended EventRequest interface to include expanded fields that might come from the API
@ -272,6 +273,11 @@ const EventRequestModal: React.FC<EventRequestModalProps> = ({ eventRequests })
const dataSync = DataSyncService.getInstance();
await dataSync.syncCollection(Collections.EVENT_REQUESTS);
// Find the request to get its name and previous status
const request = localEventRequests.find((req) => req.id === id);
const eventName = request?.name || "Event";
const previousStatus = request?.status;
// Update local state
setLocalEventRequests(prevRequests =>
prevRequests.map(req =>
@ -279,13 +285,18 @@ const EventRequestModal: React.FC<EventRequestModalProps> = ({ eventRequests })
)
);
// Find the request to get its name
const request = localEventRequests.find((req) => req.id === id);
const eventName = request?.name || "Event";
// Notify success
toast.success(`"${eventName}" status updated to ${status}`);
// Send email notification for status change (non-blocking)
try {
await EmailClient.notifyEventRequestStatusChange(id, previousStatus || 'unknown', status);
console.log('Event request status change notification email sent successfully');
} catch (emailError) {
console.error('Failed to send event request status change notification email:', emailError);
// Don't show error to user - email failure shouldn't disrupt the main operation
}
// Dispatch event for other components
document.dispatchEvent(
new CustomEvent("status-updated", {

View file

@ -370,7 +370,7 @@ export default function EmailRequestSettings() {
<div className="p-4 bg-base-200 rounded-lg">
<p className="text-sm">
If you have any questions or need help with your IEEE email, please contact <a href="mailto:webmaster@ieeeucsd.org" className="underline">webmaster@ieeeucsd.org</a>
If you have any questions or need help with your IEEE email, please contact <a href="mailto:webmaster@ieeeatucsd.org" className="underline">webmaster@ieeeatucsd.org</a>
</p>
</div>
</div>

View file

@ -292,7 +292,7 @@ async function sendCredentialsEmail(
Please change your password after your first login.
If you have any questions, please contact webmaster@ieeeucsd.org.
If you have any questions, please contact webmaster@ieeeatucsd.org.
Best regards,
IEEE UCSD Web Team
@ -311,7 +311,7 @@ async function sendWebmasterNotification(
) {
// In a real implementation, you would use an email service
console.log(`
To: webmaster@ieeeucsd.org
To: webmaster@ieeeatucsd.org
Subject: New IEEE Email Account Created
A new IEEE email account has been created:

View file

@ -7,6 +7,7 @@ export const POST: APIRoute = async ({ request }) => {
const {
type,
reimbursementId,
eventRequestId,
previousStatus,
newStatus,
changedByUserId,
@ -20,6 +21,7 @@ export const POST: APIRoute = async ({ request }) => {
console.log('📋 Request data:', {
type,
reimbursementId,
eventRequestId,
hasAuthData: !!authData,
authDataHasToken: !!(authData?.token),
authDataHasModel: !!(authData?.model),
@ -28,10 +30,10 @@ export const POST: APIRoute = async ({ request }) => {
isPrivate
});
if (!type || !reimbursementId) {
if (!type || (!reimbursementId && !eventRequestId)) {
console.error('❌ Missing required parameters');
return new Response(
JSON.stringify({ error: 'Missing required parameters: type and reimbursementId' }),
JSON.stringify({ error: 'Missing required parameters: type and (reimbursementId or eventRequestId)' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
@ -66,9 +68,9 @@ export const POST: APIRoute = async ({ request }) => {
switch (type) {
case 'status_change':
if (!newStatus) {
if (!newStatus || !reimbursementId) {
return new Response(
JSON.stringify({ error: 'Missing newStatus for status_change notification' }),
JSON.stringify({ error: 'Missing newStatus or reimbursementId for status_change notification' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
@ -82,10 +84,10 @@ export const POST: APIRoute = async ({ request }) => {
break;
case 'comment':
if (!comment || !commentByUserId) {
console.error('❌ Missing comment or commentByUserId for comment notification');
if (!comment || !commentByUserId || !reimbursementId) {
console.error('❌ Missing comment, commentByUserId, or reimbursementId for comment notification');
return new Response(
JSON.stringify({ error: 'Missing comment or commentByUserId for comment notification' }),
JSON.stringify({ error: 'Missing comment, commentByUserId, or reimbursementId for comment notification' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
@ -98,11 +100,70 @@ export const POST: APIRoute = async ({ request }) => {
break;
case 'submission':
if (!reimbursementId) {
return new Response(
JSON.stringify({ error: 'Missing reimbursementId for submission notification' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
success = await sendSubmissionEmail(pb, resend, fromEmail, replyToEmail, {
reimbursementId
});
break;
case 'event_request_submission':
if (!eventRequestId) {
return new Response(
JSON.stringify({ error: 'Missing eventRequestId for event request submission notification' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
success = await sendEventRequestSubmissionEmail(pb, resend, fromEmail, replyToEmail, {
eventRequestId
});
break;
case 'event_request_status_change':
if (!eventRequestId || !newStatus) {
return new Response(
JSON.stringify({ error: 'Missing eventRequestId or newStatus for event request status change notification' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
success = await sendEventRequestStatusChangeEmail(pb, resend, fromEmail, replyToEmail, {
eventRequestId,
newStatus,
previousStatus,
changedByUserId,
declinedReason: additionalContext?.declinedReason
});
break;
case 'pr_completed':
if (!eventRequestId) {
return new Response(
JSON.stringify({ error: 'Missing eventRequestId for PR completed notification' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
success = await sendPRCompletedEmail(pb, resend, fromEmail, replyToEmail, {
eventRequestId
});
break;
case 'design_pr_notification':
if (!eventRequestId) {
return new Response(
JSON.stringify({ error: 'Missing eventRequestId for design PR notification' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
success = await sendDesignPRNotificationEmail(pb, resend, fromEmail, replyToEmail, {
eventRequestId,
action: additionalContext?.action || 'unknown'
});
break;
case 'test':
const { email } = additionalContext || {};
if (!email) {
@ -608,6 +669,746 @@ async function sendTestEmail(resend: any, fromEmail: string, replyToEmail: strin
}
}
async function sendEventRequestSubmissionEmail(pb: any, resend: any, fromEmail: string, replyToEmail: string, data: any): Promise<boolean> {
try {
console.log('🎪 Starting event request submission email process...');
console.log('Environment check:', {
hasResendKey: !!import.meta.env.RESEND_API_KEY,
fromEmail,
replyToEmail,
pocketbaseUrl: import.meta.env.POCKETBASE_URL
});
// Get event request details
console.log('🔍 Fetching event request details for:', data.eventRequestId);
const eventRequest = await pb.collection('event_request').getOne(data.eventRequestId);
console.log('✅ Event request fetched:', { id: eventRequest.id, name: eventRequest.name });
// Get submitter user details
console.log('👤 Fetching user details for:', eventRequest.requested_user);
const user = await pb.collection('users').getOne(eventRequest.requested_user);
if (!user) {
console.error('❌ User not found:', eventRequest.requested_user);
return false;
}
console.log('✅ User fetched:', { id: user.id, name: user.name, email: user.email });
const coordinatorsEmail = 'coordinators@ieeeatucsd.org';
const subject = `New Event Request Submitted: ${eventRequest.name}`;
console.log('📝 Email details:', {
to: coordinatorsEmail,
subject,
submittedBy: user.name
});
// Format date/time for display
const formatDateTime = (dateString: string) => {
try {
const date = new Date(dateString);
return date.toLocaleString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
timeZoneName: 'short'
});
} catch (e) {
return dateString;
}
};
// Format flyer types for display
const formatFlyerTypes = (flyerTypes: string[]) => {
if (!flyerTypes || flyerTypes.length === 0) return 'None specified';
const typeMap: Record<string, string> = {
'digital_with_social': 'Digital with Social Media',
'digital_no_social': 'Digital without Social Media',
'physical_with_advertising': 'Physical with Advertising',
'physical_no_advertising': 'Physical without Advertising',
'newsletter': 'Newsletter',
'other': 'Other'
};
return flyerTypes.map(type => typeMap[type] || type).join(', ');
};
// Format required logos for display
const formatLogos = (logos: string[]) => {
if (!logos || logos.length === 0) return 'None specified';
return logos.join(', ');
};
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${subject}</title>
</head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 10px; margin-bottom: 30px;">
<h1 style="color: white; margin: 0; font-size: 24px;">🎪 New Event Request Submitted</h1>
</div>
<div style="background: #f8f9fa; padding: 25px; border-radius: 10px; margin-bottom: 25px;">
<h2 style="margin-top: 0; color: #2c3e50;">Event Request Details</h2>
<p>Hello Coordinators,</p>
<p>A new event request has been submitted by <strong>${user.name}</strong> and requires your review.</p>
<div style="background: white; padding: 20px; border-radius: 8px; border-left: 4px solid #28a745; margin: 20px 0;">
<h3 style="margin-top: 0; color: #155724;">Basic Information</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold; width: 30%;">Event Name:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${eventRequest.name}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Location:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${eventRequest.location}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Start Date & Time:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${formatDateTime(eventRequest.start_date_time)}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">End Date & Time:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${formatDateTime(eventRequest.end_date_time)}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Expected Attendance:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${eventRequest.expected_attendance || 'Not specified'}</td>
</tr>
<tr>
<td style="padding: 8px 0; font-weight: bold;">Submitted By:</td>
<td style="padding: 8px 0;">${user.name} (${user.email})</td>
</tr>
</table>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; border-left: 4px solid #17a2b8; margin: 20px 0;">
<h3 style="margin-top: 0; color: #0c5460;">Event Description</h3>
<div style="background: #f8f9fa; padding: 15px; border-radius: 6px;">
<p style="margin: 0; white-space: pre-wrap;">${eventRequest.event_description || 'No description provided'}</p>
</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; border-left: 4px solid #6f42c1; margin: 20px 0;">
<h3 style="margin-top: 0; color: #4b2982;">PR & Marketing Requirements</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold; width: 30%;">Flyers Needed:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">
<span style="background: ${eventRequest.flyers_needed ? '#28a745' : '#dc3545'}; color: white; padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: 500;">
${eventRequest.flyers_needed ? 'Yes' : 'No'}
</span>
</td>
</tr>
${eventRequest.flyers_needed ? `
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Flyer Types:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${formatFlyerTypes(eventRequest.flyer_type)}</td>
</tr>
${eventRequest.flyer_advertising_start_date ? `
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Advertising Start:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${formatDateTime(eventRequest.flyer_advertising_start_date)}</td>
</tr>
` : ''}
${eventRequest.required_logos ? `
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Required Logos:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${formatLogos(eventRequest.required_logos)}</td>
</tr>
` : ''}
` : ''}
<tr>
<td style="padding: 8px 0; font-weight: bold;">Photography Needed:</td>
<td style="padding: 8px 0;">
<span style="background: ${eventRequest.photography_needed ? '#28a745' : '#dc3545'}; color: white; padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: 500;">
${eventRequest.photography_needed ? 'Yes' : 'No'}
</span>
</td>
</tr>
</table>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; border-left: 4px solid #ffc107; margin: 20px 0;">
<h3 style="margin-top: 0; color: #856404;">Logistics & Funding</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold; width: 30%;">AS Funding Required:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">
<span style="background: ${eventRequest.as_funding_required ? '#28a745' : '#dc3545'}; color: white; padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: 500;">
${eventRequest.as_funding_required ? 'Yes' : 'No'}
</span>
</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Food/Drinks Served:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">
<span style="background: ${eventRequest.food_drinks_being_served ? '#28a745' : '#dc3545'}; color: white; padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: 500;">
${eventRequest.food_drinks_being_served ? 'Yes' : 'No'}
</span>
</td>
</tr>
<tr>
<td style="padding: 8px 0; font-weight: bold;">Room Booking:</td>
<td style="padding: 8px 0;">
<span style="background: ${eventRequest.will_or_have_room_booking ? '#28a745' : '#dc3545'}; color: white; padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: 500;">
${eventRequest.will_or_have_room_booking ? 'Has Booking' : 'No Booking'}
</span>
</td>
</tr>
</table>
</div>
<div style="background: #d4edda; padding: 15px; border-radius: 8px; border-left: 4px solid #28a745; margin: 20px 0;">
<h4 style="margin: 0 0 10px 0; color: #155724;">Next Steps</h4>
<ul style="margin: 0; padding-left: 20px; color: #155724;">
<li>Review the event request details in the dashboard</li>
<li>Coordinate with the submitter if clarification is needed</li>
<li>Assign tasks to appropriate team members (Internal, Events, Projects, etc)</li>
<li>Update the event request status once processed</li>
</ul>
</div>
</div>
<div style="text-align: center; padding: 20px; border-top: 1px solid #eee; color: #666; font-size: 14px;">
<p>This is an automated notification from IEEE UCSD Event Management System.</p>
<p>Event Request ID: ${eventRequest.id}</p>
<p>If you have any questions, please contact the submitter at <a href="mailto:${user.email}" style="color: #667eea;">${user.email}</a></p>
</div>
</body>
</html>
`;
console.log('📤 Attempting to send event request notification email via Resend...');
const result = await resend.emails.send({
from: fromEmail,
to: [coordinatorsEmail],
replyTo: user.email, // Set reply-to as the submitter for easy communication
subject,
html,
});
console.log('✅ Resend event request notification response:', result);
console.log('🎉 Event request notification email sent successfully!');
return true;
} catch (error) {
console.error('❌ Failed to send event request notification email:', error);
console.error('Event request email error details:', {
name: error instanceof Error ? error.name : 'Unknown',
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined
});
return false;
}
}
async function sendEventRequestStatusChangeEmail(pb: any, resend: any, fromEmail: string, replyToEmail: string, data: any): Promise<boolean> {
try {
console.log('🎯 Starting event request status change email process...');
console.log('Environment check:', {
hasResendKey: !!import.meta.env.RESEND_API_KEY,
fromEmail,
replyToEmail,
pocketbaseUrl: import.meta.env.POCKETBASE_URL
});
// Get event request details
console.log('🔍 Fetching event request details for:', data.eventRequestId);
const eventRequest = await pb.collection('event_request').getOne(data.eventRequestId);
console.log('✅ Event request fetched:', { id: eventRequest.id, name: eventRequest.name });
// Get submitter user details
console.log('👤 Fetching user details for:', eventRequest.requested_user);
const user = await pb.collection('users').getOne(eventRequest.requested_user);
if (!user) {
console.error('❌ User not found:', eventRequest.requested_user);
return false;
}
console.log('✅ User fetched:', { id: user.id, name: user.name, email: user.email });
const coordinatorsEmail = 'coordinators@ieeeatucsd.org';
// Format date/time for display
const formatDateTime = (dateString: string) => {
try {
const date = new Date(dateString);
return date.toLocaleString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
timeZoneName: 'short'
});
} catch (e) {
return dateString;
}
};
// Email 1: Send to User (Submitter)
const userSubject = `Your Event Request Status Updated: ${eventRequest.name}`;
const userHtml = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${userSubject}</title>
</head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 10px; margin-bottom: 30px;">
<h1 style="color: white; margin: 0; font-size: 24px;">IEEE UCSD Event Request Update</h1>
</div>
<div style="background: #f8f9fa; padding: 25px; border-radius: 10px; margin-bottom: 25px;">
<h2 style="margin-top: 0; color: #2c3e50;">Status Update</h2>
<p>Hello ${user.name},</p>
<p>Your event request "<strong>${eventRequest.name}</strong>" has been updated.</p>
<div style="background: white; padding: 20px; border-radius: 8px; border-left: 4px solid ${getStatusColor(data.newStatus)}; margin: 20px 0;">
<div style="margin-bottom: 15px;">
<span style="font-weight: bold; color: #666;">Status:</span>
<span style="background: ${getStatusColor(data.newStatus)}; color: white; padding: 6px 12px; border-radius: 20px; font-size: 14px; font-weight: 500; margin-left: 10px;">${getStatusText(data.newStatus)}</span>
</div>
${data.previousStatus && data.previousStatus !== data.newStatus ? `
<div style="color: #666; font-size: 14px;">
Changed from: <span style="text-decoration: line-through;">${getStatusText(data.previousStatus)}</span> <strong>${getStatusText(data.newStatus)}</strong>
</div>
` : ''}
${data.newStatus === 'declined' && data.declinedReason ? `
<div style="background: #f8d7da; padding: 15px; border-radius: 8px; border-left: 4px solid #dc3545; margin: 15px 0;">
<p style="margin: 0; color: #721c24;"><strong>Decline Reason:</strong></p>
<p style="margin: 5px 0 0 0; color: #721c24;">${data.declinedReason}</p>
</div>
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; border-left: 4px solid #ffc107; margin: 15px 0;">
<p style="margin: 0; color: #856404;"><strong>Next Steps:</strong> Please address the concerns mentioned above and resubmit your event request with the proper information.</p>
</div>
` : ''}
</div>
<div style="margin: 25px 0;">
<h3 style="color: #2c3e50; margin-bottom: 15px;">Your Event Request Details</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold; width: 30%;">Event Name:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${eventRequest.name}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Status:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${getStatusText(data.newStatus)}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Location:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${eventRequest.location}</td>
</tr>
<tr>
<td style="padding: 8px 0; font-weight: bold;">Event Date:</td>
<td style="padding: 8px 0;">${formatDateTime(eventRequest.start_date_time)}</td>
</tr>
</table>
</div>
</div>
<div style="text-align: center; padding: 20px; border-top: 1px solid #eee; color: #666; font-size: 14px;">
<p>This is an automated notification from IEEE UCSD Event Management System.</p>
<p>Event Request ID: ${eventRequest.id}</p>
<p>If you have any questions, please contact us at <a href="mailto:coordinators@ieeeatucsd.org" style="color: #667eea;">coordinators@ieeeatucsd.org</a></p>
</div>
</body>
</html>
`;
// Email 2: Send to Coordinators
const coordinatorSubject = `Event Request Status Updated: ${eventRequest.name}`;
const coordinatorHtml = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${coordinatorSubject}</title>
</head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 10px; margin-bottom: 30px;">
<h1 style="color: white; margin: 0; font-size: 24px;">IEEE UCSD Event Request Update</h1>
</div>
<div style="background: #f8f9fa; padding: 25px; border-radius: 10px; margin-bottom: 25px;">
<h2 style="margin-top: 0; color: #2c3e50;">Event Request Status Updated</h2>
<p>Hello Coordinators,</p>
<p>The status of the event request "<strong>${eventRequest.name}</strong>" has been updated.</p>
<div style="background: white; padding: 20px; border-radius: 8px; border-left: 4px solid ${getStatusColor(data.newStatus)}; margin: 20px 0;">
<div style="margin-bottom: 15px;">
<span style="font-weight: bold; color: #666;">Status:</span>
<span style="background: ${getStatusColor(data.newStatus)}; color: white; padding: 6px 12px; border-radius: 20px; font-size: 14px; font-weight: 500; margin-left: 10px;">${getStatusText(data.newStatus)}</span>
</div>
${data.previousStatus && data.previousStatus !== data.newStatus ? `
<div style="color: #666; font-size: 14px;">
Changed from: <span style="text-decoration: line-through;">${getStatusText(data.previousStatus)}</span> <strong>${getStatusText(data.newStatus)}</strong>
</div>
` : ''}
${data.newStatus === 'declined' && data.declinedReason ? `
<div style="background: #f8d7da; padding: 15px; border-radius: 8px; border-left: 4px solid #dc3545; margin: 15px 0;">
<p style="margin: 0; color: #721c24;"><strong>Decline Reason Provided:</strong></p>
<p style="margin: 5px 0 0 0; color: #721c24;">${data.declinedReason}</p>
</div>
` : ''}
</div>
<div style="margin: 25px 0;">
<h3 style="color: #2c3e50; margin-bottom: 15px;">Event Request Details</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold; width: 30%;">Event Name:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${eventRequest.name}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Status:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${getStatusText(data.newStatus)}</td>
</tr>
<tr>
<td style="padding: 8px 0; font-weight: bold;">Submitted By:</td>
<td style="padding: 8px 0;">${user.name} (${user.email})</td>
</tr>
</table>
</div>
</div>
<div style="text-align: center; padding: 20px; border-top: 1px solid #eee; color: #666; font-size: 14px;">
<p>This is an automated notification from IEEE UCSD Event Management System.</p>
<p>Event Request ID: ${eventRequest.id}</p>
<p>If you have any questions, please contact the submitter at <a href="mailto:${user.email}" style="color: #667eea;">${user.email}</a></p>
</div>
</body>
</html>
`;
console.log('📤 Attempting to send event request status change emails via Resend...');
// Send email to user
console.log('📧 Sending to user:', user.email);
const userResult = await resend.emails.send({
from: fromEmail,
to: [user.email],
replyTo: replyToEmail,
subject: userSubject,
html: userHtml,
});
// Send email to coordinators
console.log('📧 Sending to coordinators:', coordinatorsEmail);
const coordinatorResult = await resend.emails.send({
from: fromEmail,
to: [coordinatorsEmail],
replyTo: user.email, // Set reply-to as the submitter for easy communication
subject: coordinatorSubject,
html: coordinatorHtml,
});
console.log('✅ Resend user email response:', userResult);
console.log('✅ Resend coordinator email response:', coordinatorResult);
console.log('🎉 Event request status change emails sent successfully!');
return true;
} catch (error) {
console.error('❌ Failed to send event request status change email:', error);
console.error('Event request status change email error details:', {
name: error instanceof Error ? error.name : 'Unknown',
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined
});
return false;
}
}
async function sendPRCompletedEmail(pb: any, resend: any, fromEmail: string, replyToEmail: string, data: any): Promise<boolean> {
try {
console.log('🎨 Starting PR completed email process...');
console.log('Environment check:', {
hasResendKey: !!import.meta.env.RESEND_API_KEY,
fromEmail,
replyToEmail,
pocketbaseUrl: import.meta.env.POCKETBASE_URL
});
// Get event request details
console.log('🔍 Fetching event request details for:', data.eventRequestId);
const eventRequest = await pb.collection('event_request').getOne(data.eventRequestId);
console.log('✅ Event request fetched:', { id: eventRequest.id, name: eventRequest.name });
// Get submitter user details
console.log('👤 Fetching user details for:', eventRequest.requested_user);
const user = await pb.collection('users').getOne(eventRequest.requested_user);
if (!user || !user.email) {
console.error('❌ User not found or no email:', eventRequest.requested_user);
return false;
}
console.log('✅ User fetched:', { id: user.id, name: user.name, email: user.email });
const subject = `PR Materials Completed for Your Event: ${eventRequest.name}`;
console.log('📝 Email details:', {
to: user.email,
subject,
eventName: eventRequest.name
});
// Format date/time for display
const formatDateTime = (dateString: string) => {
try {
const date = new Date(dateString);
return date.toLocaleString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
timeZoneName: 'short'
});
} catch (e) {
return dateString;
}
};
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${subject}</title>
</head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(135deg, #28a745 0%, #20c997 100%); padding: 30px; border-radius: 10px; margin-bottom: 30px;">
<h1 style="color: white; margin: 0; font-size: 24px;">🎨 PR Materials Completed!</h1>
</div>
<div style="background: #f8f9fa; padding: 25px; border-radius: 10px; margin-bottom: 25px;">
<h2 style="margin-top: 0; color: #2c3e50;">Great News!</h2>
<p>Hello ${user.name},</p>
<p>The PR materials for your event "<strong>${eventRequest.name}</strong>" have been completed by our PR team!</p>
<div style="background: white; padding: 20px; border-radius: 8px; border-left: 4px solid #28a745; margin: 20px 0;">
<div style="margin-bottom: 15px;">
<span style="background: #28a745; color: white; padding: 6px 12px; border-radius: 20px; font-size: 14px; font-weight: 500;">
PR Materials Completed
</span>
</div>
<h3 style="margin-top: 0; color: #155724;">Event Details</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold; width: 30%;">Event Name:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${eventRequest.name}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Location:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${eventRequest.location}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Event Date:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${formatDateTime(eventRequest.start_date_time)}</td>
</tr>
<tr>
<td style="padding: 8px 0; font-weight: bold;">Flyers Needed:</td>
<td style="padding: 8px 0;">${eventRequest.flyers_needed ? 'Yes' : 'No'}</td>
</tr>
</table>
</div>
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; border-left: 4px solid #ffc107; margin: 20px 0;">
<h4 style="margin: 0 0 10px 0; color: #856404;">📞 Next Steps</h4>
<p style="margin: 0; color: #856404;">
<strong>Important:</strong> Please reach out to the Internal team to coordinate any remaining logistics for your event.
They will help ensure everything is ready for your event date.
</p>
<p style="margin: 10px 0 0 0; color: #856404;">
Contact: <strong>internal@ieeeatucsd.org</strong>
</p>
</div>
</div>
<div style="text-align: center; padding: 20px; border-top: 1px solid #eee; color: #666; font-size: 14px;">
<p>This is an automated notification from IEEE UCSD Event Management System.</p>
<p>Event Request ID: ${eventRequest.id}</p>
<p>If you have any questions about your PR materials, please contact us at <a href="mailto:${replyToEmail}" style="color: #667eea;">${replyToEmail}</a></p>
</div>
</body>
</html>
`;
console.log('📤 Attempting to send PR completed email via Resend...');
const result = await resend.emails.send({
from: fromEmail,
to: [user.email],
replyTo: replyToEmail,
subject,
html,
});
console.log('✅ Resend PR completed response:', result);
console.log('🎉 PR completed email sent successfully!');
return true;
} catch (error) {
console.error('❌ Failed to send PR completed email:', error);
console.error('PR completed email error details:', {
name: error instanceof Error ? error.name : 'Unknown',
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined
});
return false;
}
}
async function sendDesignPRNotificationEmail(pb: any, resend: any, fromEmail: string, replyToEmail: string, data: any): Promise<boolean> {
try {
console.log('🎨 Starting design PR notification email process...');
console.log('Environment check:', {
hasResendKey: !!import.meta.env.RESEND_API_KEY,
fromEmail,
replyToEmail,
pocketbaseUrl: import.meta.env.POCKETBASE_URL
});
// Get event request details
console.log('🔍 Fetching event request details for:', data.eventRequestId);
const eventRequest = await pb.collection('event_request').getOne(data.eventRequestId);
console.log('✅ Event request fetched:', { id: eventRequest.id, name: eventRequest.name });
// Get submitter user details
console.log('👤 Fetching user details for:', eventRequest.requested_user);
const user = await pb.collection('users').getOne(eventRequest.requested_user);
if (!user) {
console.error('❌ User not found:', eventRequest.requested_user);
return false;
}
console.log('✅ User fetched:', { id: user.id, name: user.name, email: user.email });
const designEmail = 'design@ieeeatucsd.org';
let subject = '';
let actionMessage = '';
switch (data.action) {
case 'submission':
subject = `New Event Request with PR Materials: ${eventRequest.name}`;
actionMessage = 'A new event request has been submitted that requires PR materials.';
break;
case 'pr_update':
subject = `PR Materials Updated: ${eventRequest.name}`;
actionMessage = 'The PR materials for this event request have been updated.';
break;
case 'declined':
subject = `Event Request Declined - PR Work Cancelled: ${eventRequest.name}`;
actionMessage = 'This event request has been declined. Please ignore any pending PR work for this event.';
break;
default:
subject = `Event Request PR Notification: ${eventRequest.name}`;
actionMessage = 'There has been an update to an event request requiring PR materials.';
}
console.log('📝 Email details:', {
to: designEmail,
subject,
action: data.action
});
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${subject}</title>
</head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 10px; margin-bottom: 30px;">
<h1 style="color: white; margin: 0; font-size: 24px;">🎨 IEEE UCSD Design Team Notification</h1>
</div>
<div style="background: #f8f9fa; padding: 25px; border-radius: 10px; margin-bottom: 25px;">
<h2 style="margin-top: 0; color: #2c3e50;">PR Materials ${data.action === 'declined' ? 'Cancelled' : 'Required'}</h2>
<p>Hello Design Team,</p>
<p>${actionMessage}</p>
<div style="margin: 25px 0;">
<h3 style="color: #2c3e50; margin-bottom: 15px;">Event Request Details</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold; width: 30%;">Event Name:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${eventRequest.name}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Action:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${data.action.charAt(0).toUpperCase() + data.action.slice(1)}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee; font-weight: bold;">Submitted By:</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;">${user.name} (${user.email})</td>
</tr>
<tr>
<td style="padding: 8px 0; font-weight: bold;">Event Description:</td>
<td style="padding: 8px 0;">${eventRequest.event_description}</td>
</tr>
</table>
</div>
${data.action !== 'declined' ? `
<div style="background: #d4edda; padding: 15px; border-radius: 8px; border-left: 4px solid #28a745; margin: 20px 0;">
<p style="margin: 0; color: #155724;"><strong>Next Steps:</strong> Please coordinate with the internal team for PR material creation and timeline.</p>
</div>
` : `
<div style="background: #f8d7da; padding: 15px; border-radius: 8px; border-left: 4px solid #dc3545; margin: 20px 0;">
<p style="margin: 0; color: #721c24;"><strong>Note:</strong> This event has been declined. No further PR work is needed.</p>
</div>
`}
</div>
<div style="text-align: center; padding: 20px; border-top: 1px solid #eee; color: #666; font-size: 14px;">
<p>This is an automated notification from IEEE UCSD Event Management System.</p>
<p>Event Request ID: ${eventRequest.id}</p>
<p>If you have any questions, please contact <a href="mailto:internal@ieeeatucsd.org" style="color: #667eea;">internal@ieeeatucsd.org</a></p>
</div>
</body>
</html>
`;
console.log('📤 Attempting to send design PR notification email via Resend...');
const result = await resend.emails.send({
from: fromEmail,
to: [designEmail],
replyTo: replyToEmail,
subject,
html,
});
console.log('✅ Resend design PR notification response:', result);
console.log('🎉 Design PR notification email sent successfully!');
return true;
} catch (error) {
console.error('❌ Failed to send design PR notification email:', error);
console.error('Design PR notification email error details:', {
name: error instanceof Error ? error.name : 'Unknown',
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined
});
return false;
}
}
// Helper functions
function getStatusColor(status: string): string {
switch (status) {

View file

@ -112,6 +112,7 @@ export interface EventRequest extends BaseRecord {
other_flyer_type?: string;
flyer_advertising_start_date?: string;
flyer_additional_requests?: string;
flyers_completed?: boolean; // Track if flyers have been completed by PR team
photography_needed: boolean;
required_logos?: string[]; // IEEE, AS, HKN, TESC, PIB, TNT, SWE, OTHER
other_logos?: string[]; // Array of logo IDs
@ -127,6 +128,7 @@ export interface EventRequest extends BaseRecord {
needs_graphics?: boolean;
needs_as_funding?: boolean;
status: "submitted" | "pending" | "completed" | "declined";
declined_reason?: string; // Reason for declining the event request
requested_user?: string;
}

View file

@ -6,14 +6,16 @@
import { Authentication } from '../pocketbase/Authentication';
interface EmailNotificationRequest {
type: 'status_change' | 'comment' | 'submission' | 'test';
reimbursementId: string;
type: 'status_change' | 'comment' | 'submission' | 'test' | 'event_request_submission' | 'event_request_status_change' | 'pr_completed' | 'design_pr_notification';
reimbursementId?: string;
eventRequestId?: string;
previousStatus?: string;
newStatus?: string;
changedByUserId?: string;
comment?: string;
commentByUserId?: string;
isPrivate?: boolean;
declinedReason?: string;
additionalContext?: Record<string, any>;
authData?: { token: string; model: any };
}
@ -142,11 +144,64 @@ export class EmailClient {
/**
* Send test email
*/
static async sendTestEmail(email: string): Promise<boolean> {
static async sendTestEmail(): Promise<boolean> {
return this.sendEmailNotification({
type: 'test',
reimbursementId: 'test', // Required but not used for test emails
additionalContext: { email }
reimbursementId: 'test' // Required but not used for test emails
});
}
/**
* Send event request submission notification to coordinators
*/
static async notifyEventRequestSubmission(eventRequestId: string): Promise<boolean> {
return this.sendEmailNotification({
type: 'event_request_submission',
eventRequestId
});
}
/**
* Send email notification when an event request status is changed
*/
static async notifyEventRequestStatusChange(
eventRequestId: string,
previousStatus: string,
newStatus: string,
changedByUserId?: string,
declinedReason?: string
): Promise<boolean> {
return this.sendEmailNotification({
type: 'event_request_status_change',
eventRequestId,
previousStatus,
newStatus,
changedByUserId,
declinedReason
});
}
/**
* Send email notification when PR work is completed for an event request
*/
static async notifyPRCompleted(eventRequestId: string): Promise<boolean> {
return this.sendEmailNotification({
type: 'pr_completed',
eventRequestId
});
}
/**
* Send email notification to design team for PR-related actions
*/
static async notifyDesignTeam(
eventRequestId: string,
action: 'submission' | 'pr_update' | 'declined'
): Promise<boolean> {
return this.sendEmailNotification({
type: 'design_pr_notification',
eventRequestId,
additionalContext: { action }
});
}
}

View file

@ -1,6 +1,6 @@
# Email Notification System
This directory contains the email notification system for the IEEE UCSD reimbursement portal using Resend.
This directory contains the email notification system for the IEEE UCSD reimbursement portal and event management system using Resend.
## Setup
@ -34,11 +34,15 @@ REPLY_TO_EMAIL="treasurer@ieeeucsd.org"
The system automatically sends emails for the following events:
#### Reimbursement System
1. **Reimbursement Submitted** - Confirmation email when a user submits a new reimbursement request
2. **Status Changes** - Notification when reimbursement status is updated (submitted, under review, approved, rejected, in progress, paid)
3. **Comments Added** - Notification when someone adds a public comment to a reimbursement
4. **Rejections with Reasons** - Detailed rejection notification including the specific reason for rejection
#### Event Management System
1. **Event Request Submitted** - Notification to coordinators@ieeeatucsd.org when a new event request is submitted
Note: Private comments are not sent via email to maintain privacy.
### Email Templates
@ -47,7 +51,7 @@ All emails include:
- Professional IEEE UCSD branding
- Responsive design for mobile and desktop
- Clear status indicators with color coding
- Reimbursement details summary
- Detailed information summary
- Next steps information
- Contact information for support
@ -55,6 +59,7 @@ All emails include:
### In React Components (Client-side)
#### Reimbursement Notifications
```typescript
import { EmailClient } from '../../../scripts/email/EmailClient';
@ -83,10 +88,19 @@ await EmailClient.notifyStatusChange(
);
```
#### Event Request Notifications
```typescript
import { EmailClient } from '../../../scripts/email/EmailClient';
// Send event request submission notification to coordinators
await EmailClient.notifyEventRequestSubmission(eventRequestId);
```
### API Route (Server-side)
The API route at `/api/email/send-reimbursement-notification` accepts POST requests with the following structure:
#### Reimbursement Notifications
```json
{
"type": "status_change" | "comment" | "submission" | "test",
@ -105,6 +119,18 @@ The API route at `/api/email/send-reimbursement-notification` accepts POST reque
}
```
#### Event Request Notifications
```json
{
"type": "event_request_submission",
"eventRequestId": "string",
"authData": { // Authentication data for PocketBase access
"token": "string",
"model": {}
}
}
```
## Architecture
The email system uses a client-server architecture for security and authentication:
@ -128,9 +154,15 @@ This ensures that:
- The server can access protected PocketBase collections
- Email operations respect user permissions and data security
## Email Recipients
- **Reimbursement notifications**: Sent to the user who submitted the reimbursement
- **Event request notifications**: Sent to coordinators@ieeeatucsd.org
- **Test emails**: Sent to the specified email address
## Error Handling
Email failures are logged but do not prevent the main operations from completing. This ensures that reimbursement processing continues even if email delivery fails.
Email failures are logged but do not prevent the main operations from completing. This ensures that reimbursement processing and event request submissions continue even if email delivery fails.
## Security
@ -139,4 +171,33 @@ Email failures are logged but do not prevent the main operations from completing
- Email addresses are validated before sending
- Private comments are not sent via email (configurable)
- All emails include appropriate contact information
- PocketBase collection access respects authentication and permissions
- PocketBase collection access respects authentication and permissions
## Event Request Email Notifications
### Event Request Submission
When a new event request is submitted, an email is automatically sent to the coordinators team.
```typescript
await EmailClient.notifyEventRequestSubmission(eventRequestId);
```
### Event Request Status Change
When an event request status is changed, an email is sent to coordinators.
```typescript
await EmailClient.notifyEventRequestStatusChange(eventRequestId, previousStatus, newStatus, changedByUserId);
```
### PR Completion Notification
When PR materials are completed for an event request, an email is sent to the submitter notifying them to contact the internal team.
```typescript
await EmailClient.notifyPRCompleted(eventRequestId);
```
This email includes:
- Confirmation that PR materials are completed
- Event details and information
- Instructions to contact the internal team for next steps
- Contact information for internal@ieeeucsd.org