add event request notifications
This commit is contained in:
parent
5d92bcfd1b
commit
aac2837b78
12 changed files with 1614 additions and 103 deletions
188
bun.lock
188
bun.lock
|
@ -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=="],
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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", {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
Loading…
Reference in a new issue