diff --git a/package-lock.json b/package-lock.json index 933c03c..79fbee3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1557,34 +1556,6 @@ "node": ">=6.9.0" } }, - "node_modules/@capacitor-community/admob": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@capacitor-community/admob/-/admob-7.0.3.tgz", - "integrity": "sha512-aogZipDosRZB5+RWNukKI1DhPnp7nZNw7KJqnL8Ii6rgYGJaCjpeBvN9bJjFbl1K2jNnCe5lp/a/YlLonBkUQA==", - "license": "MIT", - "dependencies": { - "@capacitor/core": "^7.0.0", - "@rdlabo/capacitor-docgen": "^0.4.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@capacitor-community/sqlite": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-7.0.2.tgz", - "integrity": "sha512-TkxvuLgVCSmme/NSUEE2xopTVMwMqdT6+zH9pH8g76DMdi2z+ZsPEATQnJchGw4YAhhiQ8vTYxIP9se3/zkhGg==", - "license": "MIT", - "dependencies": { - "jeep-sqlite": "^2.7.2" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@capacitor/core": ">=7.0.0" - } - }, "node_modules/@capacitor/android": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-7.4.4.tgz", @@ -1954,23 +1925,10 @@ "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.4.tgz", "integrity": "sha512-xzjxpr+d2zwTpCaN0k+C6wKSZzWFAb9OVEUtmO72ihjr/NEDoLvsGl4WLfjWPcCO2zOy0b2X52tfRWjECFUjtw==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.1.0" } }, - "node_modules/@capacitor/filesystem": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-7.1.4.tgz", - "integrity": "sha512-BdUnOVulHAtruW2GeC9o7e9LO5aFcVYqNn3dLypJSOck/WipUFNfI0QWoUS0FVGeqBbDJgFGi3zjXJx0lzbDkA==", - "license": "MIT", - "dependencies": { - "@capacitor/synapse": "^1.0.3" - }, - "peerDependencies": { - "@capacitor/core": ">=7.0.0" - } - }, "node_modules/@capacitor/ios": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-7.4.4.tgz", @@ -1998,12 +1956,6 @@ "@capacitor/core": ">=7.0.0" } }, - "node_modules/@capacitor/synapse": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@capacitor/synapse/-/synapse-1.0.4.tgz", - "integrity": "sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==", - "license": "ISC" - }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -3562,37 +3514,6 @@ "prettier": ">=2.4.0" } }, - "node_modules/@rdlabo/capacitor-docgen": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@rdlabo/capacitor-docgen/-/capacitor-docgen-0.4.1.tgz", - "integrity": "sha512-zUB0lwha4Omqyr5URy/QaFjP6Gh2xxTKAFI0MClqrmUxQwmV8c84+wGvqAoWPCzg4jk1MsU6oiRVuTkUV7xseA==", - "license": "MIT", - "dependencies": { - "colorette": "^2.0.20", - "github-slugger": "^1.5.0", - "minimist": "^1.2.8", - "typescript": "~4.2.4" - }, - "bin": { - "docgen": "bin/docgen" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@rdlabo/capacitor-docgen/node_modules/typescript": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", - "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "node_modules/@react-aria/focus": { "version": "3.21.2", "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.2.tgz", @@ -3660,6 +3581,39 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, + "node_modules/@react-email/render": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.1.2.tgz", + "integrity": "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw==", + "license": "MIT", + "dependencies": { + "html-to-text": "^9.0.5", + "prettier": "^3.5.3", + "react-promise-suspense": "^0.3.4" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/render/node_modules/prettier": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/@react-stately/flags": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", @@ -3690,16 +3644,6 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@remix-run/router": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", - "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -4133,6 +4077,34 @@ "win32" ] }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "selderee": "^0.11.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/@selderee/plugin-htmlparser2/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, "node_modules/@sequenced_app/api": { "resolved": "packages/api", "link": true @@ -4141,133 +4113,6 @@ "resolved": "packages/app", "link": true }, - "node_modules/@stencil/core": { - "version": "4.38.2", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.2.tgz", - "integrity": "sha512-opyjA+DYAtaKmaSnuC8Bb/PH7nuO+1GhVn6amsN8XT+TlT9biptlcpz4YWETwYZ+XxtX+nLdxWbW0TVafrqsvQ==", - "license": "MIT", - "bin": { - "stencil": "bin/stencil" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=7.10.0" - }, - "optionalDependencies": { - "@rollup/rollup-darwin-arm64": "4.34.9", - "@rollup/rollup-darwin-x64": "4.34.9", - "@rollup/rollup-linux-arm64-gnu": "4.34.9", - "@rollup/rollup-linux-arm64-musl": "4.34.9", - "@rollup/rollup-linux-x64-gnu": "4.34.9", - "@rollup/rollup-linux-x64-musl": "4.34.9", - "@rollup/rollup-win32-arm64-msvc": "4.34.9", - "@rollup/rollup-win32-x64-msvc": "4.34.9" - } - }, - "node_modules/@stencil/core/node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz", - "integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@stencil/core/node_modules/@rollup/rollup-darwin-x64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz", - "integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@stencil/core/node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz", - "integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@stencil/core/node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz", - "integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@stencil/core/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz", - "integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@stencil/core/node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz", - "integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@stencil/core/node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz", - "integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@stencil/core/node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz", - "integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -4518,7 +4363,6 @@ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -4669,13 +4513,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz", - "integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==", + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", "license": "MIT", - "peer": true, "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/normalize-package-data": { @@ -4710,7 +4553,6 @@ "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4984,6 +4826,7 @@ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", + "peer": true, "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" @@ -4997,7 +4840,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5680,6 +5522,7 @@ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "license": "MIT", + "peer": true, "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", @@ -5704,6 +5547,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -5720,7 +5564,8 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/boolbase": { "version": "1.0.0", @@ -5773,12 +5618,6 @@ "node": ">=8" } }, - "node_modules/browser-fs-access": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.35.0.tgz", - "integrity": "sha512-sLoadumpRfsjprP8XzVjpQc0jK8yqHBx0PtUTGYj2fftT+P/t+uyDAQdMgGAPKD011in/O+YYGh7fIs0oG/viw==", - "license": "Apache-2.0" - }, "node_modules/browserslist": { "version": "4.27.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", @@ -5798,7 +5637,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -6122,12 +5960,6 @@ "color-support": "bin.js" } }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "license": "MIT" - }, "node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", @@ -6198,6 +6030,7 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -6484,6 +6317,7 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.6.0" } @@ -6505,6 +6339,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, "license": "MIT" }, "node_modules/cors": { @@ -7019,7 +6854,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, "funding": [ { "type": "github", @@ -7382,7 +7216,6 @@ "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -7451,7 +7284,6 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -7891,7 +7723,6 @@ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", "license": "MIT", - "peer": true, "dependencies": { "cookie": "0.7.2", "cookie-signature": "1.0.7", @@ -7932,6 +7763,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -7948,7 +7780,8 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -8107,6 +7940,7 @@ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", + "peer": true, "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", @@ -8128,6 +7962,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -8144,7 +7979,8 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/find-up": { "version": "5.0.0", @@ -8330,6 +8166,7 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -8761,21 +8598,15 @@ "dev": true, "license": "MIT" }, - "node_modules/github-slugger": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", - "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", - "license": "ISC" - }, "node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", - "license": "ISC", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "license": "BlueOak-1.0.0", "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", + "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" @@ -8803,10 +8634,10 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", - "license": "ISC", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/brace-expansion": "^5.0.0" }, @@ -9071,6 +8902,137 @@ "dev": true, "license": "ISC" }, + "node_modules/html-to-text": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", + "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", + "license": "MIT", + "dependencies": { + "@selderee/plugin-htmlparser2": "^0.11.0", + "deepmerge": "^4.3.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^8.0.2", + "selderee": "^0.11.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/html-to-text/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/html-to-text/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/html-to-text/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/htmlparser2/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -9109,6 +9071,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "license": "MIT", + "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -9162,12 +9125,6 @@ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "license": "ISC" }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" - }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -9577,7 +9534,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/is-reference": { "version": "1.2.1", @@ -9833,26 +9791,12 @@ "node": ">=10" } }, - "node_modules/jeep-sqlite": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/jeep-sqlite/-/jeep-sqlite-2.8.0.tgz", - "integrity": "sha512-FWNUP6OAmrUHwiW7H1xH5YUQ8tN2O4l4psT1sLd7DQtHd5PfrA1nvNdeKPNj+wQBtu7elJa8WoUibTytNTaaCg==", - "license": "MIT", - "dependencies": { - "@stencil/core": "^4.20.0", - "browser-fs-access": "^0.35.0", - "jszip": "^3.10.1", - "localforage": "^1.10.0", - "sql.js": "^1.11.0" - } - }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -9864,9 +9808,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -10008,54 +9952,6 @@ "node": ">=4.0" } }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/jszip/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/jszip/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "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" - } - }, - "node_modules/jszip/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/jszip/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/kareem": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", @@ -10106,6 +10002,15 @@ "node": ">8" } }, + "node_modules/leac": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", + "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -10128,15 +10033,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -10183,24 +10079,6 @@ "node": ">=4" } }, - "node_modules/localforage": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", - "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", - "license": "Apache-2.0", - "dependencies": { - "lie": "3.1.1" - } - }, - "node_modules/localforage/node_modules/lie": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -10335,6 +10213,7 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -10545,6 +10424,7 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -10637,6 +10517,7 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -10646,6 +10527,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", + "peer": true, "dependencies": { "mime-db": "^1.54.0" }, @@ -10705,6 +10587,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10799,7 +10682,6 @@ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@mongodb-js/saslprep": "^1.3.0", "bson": "^6.10.4", @@ -10856,7 +10738,6 @@ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.19.2.tgz", "integrity": "sha512-ww2T4dBV+suCbOfG5YPwj9pLCfUVyj8FEA1D3Ux1HHqutpLxGyOYEPU06iPRBW4cKr3PJfOSYsIpHWPTkz5zig==", "license": "MIT", - "peer": true, "dependencies": { "bson": "^6.10.4", "kareem": "2.6.3", @@ -11034,6 +10915,7 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -11588,12 +11470,6 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -11620,6 +11496,19 @@ "node": ">=4" } }, + "node_modules/parseley": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", + "license": "MIT", + "dependencies": { + "leac": "^0.6.0", + "peberminta": "^0.9.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -11692,6 +11581,7 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" @@ -11726,6 +11616,15 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/peberminta": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -11824,7 +11723,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -12091,6 +11989,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, "license": "MIT" }, "node_modules/prompts": { @@ -12257,6 +12156,7 @@ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", + "peer": true, "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", @@ -12305,7 +12205,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -12318,7 +12217,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -12334,6 +12232,21 @@ "dev": true, "license": "MIT" }, + "node_modules/react-promise-suspense": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz", + "integrity": "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^2.0.1" + } + }, + "node_modules/react-promise-suspense/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -12366,40 +12279,6 @@ } } }, - "node_modules/react-router-dom": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", - "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.0", - "react-router": "6.30.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/react-router-dom/node_modules/react-router": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", - "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, "node_modules/react-router/node_modules/cookie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", @@ -13004,6 +12883,18 @@ "dev": true, "license": "ISC" }, + "node_modules/resend": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/resend/-/resend-4.8.0.tgz", + "integrity": "sha512-R8eBOFQDO6dzRTDmaMEdpqrkmgSjPpVXt4nGfWsZdYOet0kqra0xgbvTES6HmCriZEXbmGk3e0DiGIaLFTFSHA==", + "license": "MIT", + "dependencies": { + "@react-email/render": "1.1.2" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -13077,7 +12968,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -13170,6 +13060,7 @@ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "license": "MIT", + "peer": true, "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", @@ -13186,6 +13077,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -13202,7 +13094,8 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/run-parallel": { "version": "1.2.0", @@ -13321,6 +13214,18 @@ "loose-envify": "^1.1.0" } }, + "node_modules/selderee": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", + "license": "MIT", + "dependencies": { + "parseley": "^0.12.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -13338,6 +13243,7 @@ "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "license": "MIT", + "peer": true, "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", @@ -13360,6 +13266,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -13376,7 +13283,8 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/serialize-javascript": { "version": "6.0.2", @@ -13392,6 +13300,7 @@ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "license": "MIT", + "peer": true, "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", @@ -13460,12 +13369,6 @@ "node": ">= 0.4" } }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "license": "MIT" - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -13853,12 +13756,6 @@ "readable-stream": "^3.0.0" } }, - "node_modules/sql.js": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.13.0.tgz", - "integrity": "sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA==", - "license": "MIT" - }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -14161,9 +14058,9 @@ } }, "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -14251,16 +14148,6 @@ "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", "license": "MIT" }, - "node_modules/tailwind-merge": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", - "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, "node_modules/tailwindcss": { "version": "3.4.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", @@ -14756,8 +14643,7 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tunnel-agent": { "version": "0.6.0", @@ -14802,6 +14688,7 @@ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", + "peer": true, "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", @@ -14890,7 +14777,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14950,9 +14836,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -15166,7 +15052,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -16023,7 +15908,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -16143,7 +16027,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -16573,6 +16456,7 @@ "mongoose": "^8.11.0", "mongoose-lean-id": "^1.0.0", "mongoose-lean-virtuals": "^1.1.0", + "resend": "^4.0.0", "typescript": "^5.9.3" }, "engines": { @@ -16955,23 +16839,18 @@ "name": "@sequenced_app/app", "version": "1.0.9", "dependencies": { - "@capacitor-community/admob": "^7.0.3", - "@capacitor-community/sqlite": "^7.0.2", "@capacitor/android": "^7.0.0", "@capacitor/core": "^7.0.0", - "@capacitor/filesystem": "^7.0.0", "@capacitor/ios": "^7.0.0", "@capacitor/local-notifications": "^7.0.0", "@capacitor/preferences": "^7.0.0", "@headlessui/react": "^2.0.4", "@heroicons/react": "^2.1.3", "@tanstack/react-query": "^5.40.1", - "@types/node": "^20.14.2", "nanoid": "^5.0.7", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router": "^7.1.1", - "tailwind-merge": "^2.3.0", "vite-plugin-pwa": "^1.0.0" }, "devDependencies": { @@ -16986,7 +16865,6 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.7", "postcss": "^8.4.38", - "react-router-dom": "^6.23.1", "tailwindcss": "^3.4.4", "typescript": "^5.4.5", "vite": "^5.2.13" diff --git a/packages/api/package.json b/packages/api/package.json index a4e5698..7c05071 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -29,6 +29,7 @@ "mongoose": "^8.11.0", "mongoose-lean-id": "^1.0.0", "mongoose-lean-virtuals": "^1.1.0", + "resend": "^4.0.0", "typescript": "^5.9.3" } } diff --git a/packages/api/src/auth/auth.controller.ts b/packages/api/src/auth/auth.controller.ts index 17cf971..871976c 100644 --- a/packages/api/src/auth/auth.controller.ts +++ b/packages/api/src/auth/auth.controller.ts @@ -4,6 +4,9 @@ import { UserService } from "@/user/user.service"; import { User } from "@/user/user.entity"; import { Request } from "express"; import sendToWebhook from "@/logging/webhook"; +import { PasswordReset } from "./passwordReset.entity"; +import crypto from "crypto"; +import { Resend } from "resend"; @Controller() export class AuthController { @@ -80,4 +83,77 @@ export class AuthController { return { message: "success" }; } + @Post("/forgot-password") + async requestPasswordReset(req: Request): Promise<{ success: true }> { + const email = typeof req.body?.email === "string" ? req.body.email.trim().toLowerCase() : ""; + if (!email) throw new BadRequest("Email is required."); + + const user = await this.userService.getUserByEmail(email); + + // Always respond success to avoid account enumeration. + if (!user?.id || !user.email) return { success: true }; + + const token = crypto.randomBytes(32).toString("hex"); + const tokenHash = crypto.createHash("sha256").update(token).digest("hex"); + const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour validity + + await PasswordReset.deleteMany({ user: user.id }); + await PasswordReset.create({ user: user.id, tokenHash, expiresAt, used: false }); + + const resendApiKey = process.env.RESEND_API_KEY; + if (!resendApiKey) throw new BadRequest("Password reset is unavailable right now. Please try again later."); + + const resend = new Resend(resendApiKey); + const frontendUrl = process.env.FRONTEND_URL || "https://sequenced.ottegi.com"; + const resetUrl = `${frontendUrl.replace(/\/$/, "")}/auth/forgotPassword?token=${token}`; + const fromEmail = process.env.RESET_FROM_EMAIL || "Sequenced "; + + await resend.emails.send({ + from: fromEmail, + to: user.email, + subject: "Reset your Sequenced password", + html: ` +
+

Reset your password

+

Hello ${user.first ?? "there"},

+

We received a request to reset your Sequenced password. Click the button below to set a new password. This link will expire in 1 hour.

+

+ Reset password +

+

If the button doesn't work, copy and paste this link into your browser:

+

${resetUrl}

+

If you didn't request this, you can safely ignore this email.

+
+ ` + }); + + return { success: true }; + } + + @Post("/reset-password") + async resetPassword(req: Request): Promise<{ success: true }> { + const token = typeof req.body?.token === "string" ? req.body.token.trim() : ""; + const password = typeof req.body?.password === "string" ? req.body.password : ""; + + if (!token || !password) { + throw new BadRequest("Reset token and password are required."); + } + + if (password.length < 8) { + throw new BadRequest("Password must be at least 8 characters long."); + } + + const tokenHash = crypto.createHash("sha256").update(token).digest("hex"); + const resetRequest = await PasswordReset.findOne({ tokenHash, used: false }).lean().exec(); + + if (!resetRequest || !resetRequest.user || resetRequest.expiresAt < new Date()) { + throw new BadRequest("This reset link is invalid or has expired."); + } + + await this.userService.updateUser(resetRequest.user as any, { password }); + await PasswordReset.updateOne({ _id: resetRequest._id }, { used: true }).exec(); + + return { success: true }; + } + } diff --git a/packages/api/src/auth/passwordReset.entity.ts b/packages/api/src/auth/passwordReset.entity.ts new file mode 100644 index 0000000..9f776a1 --- /dev/null +++ b/packages/api/src/auth/passwordReset.entity.ts @@ -0,0 +1,21 @@ +import { Entity, Model, Prop, Index } from "@/_lib/mongoose"; +import { User } from "@/user/user.entity"; + +@Entity({ timestamps: true }) +@Index({ tokenHash: 1 }, { unique: true }) +export class PasswordReset extends Model { + + id: string; + + @Prop({ type: String, required: true }) + tokenHash: string; + + @Prop({ type: Date, required: true, expires: 0 }) + expiresAt: Date; + + @Prop({ type: Boolean, default: false }) + used: boolean; + + @Prop({ type: User, required: true }) + user: User; +} diff --git a/packages/app/.env.example b/packages/app/.env.example deleted file mode 100644 index 962e1dc..0000000 --- a/packages/app/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -VITE_IOS_AD_ID= -VITE_ANDROID_AD_ID= -VITE_PORT= \ No newline at end of file diff --git a/packages/app/android/app/capacitor.build.gradle b/packages/app/android/app/capacitor.build.gradle index ef4a0f3..d209388 100644 --- a/packages/app/android/app/capacitor.build.gradle +++ b/packages/app/android/app/capacitor.build.gradle @@ -9,9 +9,6 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { - implementation project(':capacitor-community-admob') - implementation project(':capacitor-community-sqlite') - implementation project(':capacitor-filesystem') implementation project(':capacitor-local-notifications') implementation project(':capacitor-preferences') diff --git a/packages/app/android/capacitor.settings.gradle b/packages/app/android/capacitor.settings.gradle index 2399f96..38e9122 100644 --- a/packages/app/android/capacitor.settings.gradle +++ b/packages/app/android/capacitor.settings.gradle @@ -2,15 +2,6 @@ include ':capacitor-android' project(':capacitor-android').projectDir = new File('../../../node_modules/@capacitor/android/capacitor') -include ':capacitor-community-admob' -project(':capacitor-community-admob').projectDir = new File('../../../node_modules/@capacitor-community/admob/android') - -include ':capacitor-community-sqlite' -project(':capacitor-community-sqlite').projectDir = new File('../../../node_modules/@capacitor-community/sqlite/android') - -include ':capacitor-filesystem' -project(':capacitor-filesystem').projectDir = new File('../../../node_modules/@capacitor/filesystem/android') - include ':capacitor-local-notifications' project(':capacitor-local-notifications').projectDir = new File('../../../node_modules/@capacitor/local-notifications/android') diff --git a/packages/app/electron/.gitignore b/packages/app/electron/.gitignore deleted file mode 100644 index 9863f87..0000000 --- a/packages/app/electron/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -# NPM renames .gitignore to .npmignore -# In order to prevent that, we remove the initial "." -# And the CLI then renames it -app -node_modules -dist -logs - -.env -release - -!build -out/ \ No newline at end of file diff --git a/packages/app/electron/assets/appIcon.ico b/packages/app/electron/assets/appIcon.ico deleted file mode 100644 index 1aea5a5..0000000 Binary files a/packages/app/electron/assets/appIcon.ico and /dev/null differ diff --git a/packages/app/electron/assets/appIcon.png b/packages/app/electron/assets/appIcon.png deleted file mode 100644 index 7e8612e..0000000 Binary files a/packages/app/electron/assets/appIcon.png and /dev/null differ diff --git a/packages/app/electron/assets/icon.png b/packages/app/electron/assets/icon.png deleted file mode 100644 index 6c11e78..0000000 Binary files a/packages/app/electron/assets/icon.png and /dev/null differ diff --git a/packages/app/electron/assets/splash.gif b/packages/app/electron/assets/splash.gif deleted file mode 100644 index 5ee8351..0000000 Binary files a/packages/app/electron/assets/splash.gif and /dev/null differ diff --git a/packages/app/electron/assets/splash.png b/packages/app/electron/assets/splash.png deleted file mode 100644 index ce554d1..0000000 Binary files a/packages/app/electron/assets/splash.png and /dev/null differ diff --git a/packages/app/electron/build/capacitor.config.js b/packages/app/electron/build/capacitor.config.js deleted file mode 100644 index f1bc293..0000000 --- a/packages/app/electron/build/capacitor.config.js +++ /dev/null @@ -1,19 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const config = { - appId: "com.ottegi.sequenced-app", - appName: "Sequenced", - webDir: "dist", - server: { - androidScheme: "https", - }, -}; -const configElectron = {}; -const configGlobal = Object.assign(Object.assign({}, config), { electron: configElectron, plugins: { - LocalNotifications: { - smallIcon: "icon", - iconColor: "#307acf", - sound: "beep.wav", - }, - } }); -exports.default = configGlobal; diff --git a/packages/app/electron/build/embedded.provisionprofile b/packages/app/electron/build/embedded.provisionprofile deleted file mode 100644 index a459406..0000000 Binary files a/packages/app/electron/build/embedded.provisionprofile and /dev/null differ diff --git a/packages/app/electron/build/entitlements.mas.inherit.plist b/packages/app/electron/build/entitlements.mas.inherit.plist deleted file mode 100644 index 47f1c7e..0000000 --- a/packages/app/electron/build/entitlements.mas.inherit.plist +++ /dev/null @@ -1,13 +0,0 @@ - - - - com.apple.security.app-sandbox - - com.apple.security.inherit - - com.apple.security.cs.allow-jit - - com.apple.security.cs.allow-unsigned-executable-memory - - - \ No newline at end of file diff --git a/packages/app/electron/build/entitlements.mas.loginhelper.plist b/packages/app/electron/build/entitlements.mas.loginhelper.plist deleted file mode 100644 index db8fee7..0000000 --- a/packages/app/electron/build/entitlements.mas.loginhelper.plist +++ /dev/null @@ -1,10 +0,0 @@ - - - - com.apple.security.app-sandbox - - com.apple.security.inherit - - - diff --git a/packages/app/electron/build/entitlements.mas.plist b/packages/app/electron/build/entitlements.mas.plist deleted file mode 100644 index d090bec..0000000 --- a/packages/app/electron/build/entitlements.mas.plist +++ /dev/null @@ -1,17 +0,0 @@ - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-unsigned-executable-memory - - com.apple.security.cs.allow-jit - - com.apple.security.application-groups - - B9UMLF8BH7.com.ottegi.sequenced-app - - - diff --git a/packages/app/electron/build/notarize.js b/packages/app/electron/build/notarize.js deleted file mode 100644 index c82f7f8..0000000 --- a/packages/app/electron/build/notarize.js +++ /dev/null @@ -1,13 +0,0 @@ -import { notarize } from '@electron/notarize'; -import 'dotenv/config'; - -async function packageTask () { - // Package your app here, and code sign with hardened runtime - await notarize({ - appBundleId: "com.ottegi.sequenced-app", - appPath: process.env.APP_PATH, - appleId: process.env.APPLE_ID, - appleIdPassword: process.env.APPLE_ID_PASSWORD, - teamId: process.env.APPLE_TEAM_ID, - }); -} \ No newline at end of file diff --git a/packages/app/electron/build/resignAndPackage.sh b/packages/app/electron/build/resignAndPackage.sh deleted file mode 100644 index adf0298..0000000 --- a/packages/app/electron/build/resignAndPackage.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -# Name of your app. -APP="Sequenced" -# The path of your app to sign. -APP_PATH="release/mas/$APP.app" -# The path to the location you want to put the signed package. -RESULT_PATH="release/mas/$APP-mac_store.pkg" -# The name of certificates you requested. -APP_KEY="3rd Party Mac Developer Application: Ottegi LLC (B9UMLF8BH7)" -INSTALLER_KEY="3rd Party Mac Developer Installer: Ottegi LLC (B9UMLF8BH7)" -# The path of your plist files. -PARENT_PLIST="build/entitlements.mas.plist" -CHILD_PLIST="build/entitlements.mas.inherit.plist" -LOGINHELPER_PLIST="build/entitlements.mas.loginhelper.plist" -FRAMEWORKS_PATH="$APP_PATH/Contents/Frameworks" -codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Versions/A/Electron Framework" -codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Versions/A/Libraries/libffmpeg.dylib" -codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Libraries/libffmpeg.dylib" -codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework" -codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper.app/Contents/MacOS/$APP Helper" -codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper.app/" -codesign -s "$APP_KEY" -f --entitlements "$LOGINHELPER_PLIST" "$APP_PATH/Contents/Library/LoginItems/$APP Login Helper.app/Contents/MacOS/$APP Login Helper" -codesign -s "$APP_KEY" -f --entitlements "$LOGINHELPER_PLIST" "$APP_PATH/Contents/Library/LoginItems/$APP Login Helper.app/" -codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$APP_PATH/Contents/MacOS/$APP" -codesign -s "$APP_KEY" -f --entitlements "$PARENT_PLIST" "$APP_PATH" -productbuild --component "$APP_PATH" /Applications --sign "$INSTALLER_KEY" "$RESULT_PATH" - \ No newline at end of file diff --git a/packages/app/electron/build/src/index.js b/packages/app/electron/build/src/index.js deleted file mode 100644 index 7903736..0000000 --- a/packages/app/electron/build/src/index.js +++ /dev/null @@ -1,59 +0,0 @@ -"use strict"; -var _a, _b; -Object.defineProperty(exports, "__esModule", { value: true }); -const tslib_1 = require("tslib"); -const electron_1 = require("@capacitor-community/electron"); -const electron_2 = require("electron"); -const electron_is_dev_1 = tslib_1.__importDefault(require("electron-is-dev")); -const electron_unhandled_1 = tslib_1.__importDefault(require("electron-unhandled")); -const setup_1 = require("./setup"); -// Graceful handling of unhandled errors. -(0, electron_unhandled_1.default)(); -// Define our menu templates (these are optional) -const trayMenuTemplate = [new electron_2.MenuItem({ label: 'Quit App', role: 'quit' })]; -const appMenuBarMenuTemplate = [ - { role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' }, - { role: 'viewMenu' }, -]; -// Get Config options from capacitor.config -const capacitorFileConfig = (0, electron_1.getCapacitorElectronConfig)(); -// Initialize our app. You can pass menu templates into the app here. -// const myCapacitorApp = new ElectronCapacitorApp(capacitorFileConfig); -const myCapacitorApp = new setup_1.ElectronCapacitorApp(capacitorFileConfig, trayMenuTemplate, appMenuBarMenuTemplate); -// If deeplinking is enabled then we will set it up here. -if ((_a = capacitorFileConfig.electron) === null || _a === void 0 ? void 0 : _a.deepLinkingEnabled) { - (0, electron_1.setupElectronDeepLinking)(myCapacitorApp, { - customProtocol: (_b = capacitorFileConfig.electron.deepLinkingCustomProtocol) !== null && _b !== void 0 ? _b : 'mycapacitorapp', - }); -} -// If we are in Dev mode, use the file watcher components. -if (electron_is_dev_1.default) { - (0, setup_1.setupReloadWatcher)(myCapacitorApp); -} -// Run Application -(async () => { - // Wait for electron app to be ready. - await electron_2.app.whenReady(); - // Security - Set Content-Security-Policy based on whether or not we are in dev mode. - (0, setup_1.setupContentSecurityPolicy)(myCapacitorApp.getCustomURLScheme()); - // Initialize our app, build windows, and load content. - await myCapacitorApp.init(); - // Check for updates if we are in a packaged app. -})(); -// Handle when all of our windows are close (platforms have their own expectations). -electron_2.app.on('window-all-closed', function () { - // On OS X it is common for applications and their menu bar - // to stay active until the user quits explicitly with Cmd + Q - if (process.platform !== 'darwin') { - electron_2.app.quit(); - } -}); -// When the dock icon is clicked. -electron_2.app.on('activate', async function () { - // On OS X it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (myCapacitorApp.getMainWindow().isDestroyed()) { - await myCapacitorApp.init(); - } -}); -// Place all ipc or other electron api calls and custom functionality under this line diff --git a/packages/app/electron/build/src/preload.js b/packages/app/electron/build/src/preload.js deleted file mode 100644 index c817d3b..0000000 --- a/packages/app/electron/build/src/preload.js +++ /dev/null @@ -1,4 +0,0 @@ -require('./rt/electron-rt'); -////////////////////////////// -// User Defined Preload scripts below -console.log('User Preload!'); diff --git a/packages/app/electron/build/src/rt/electron-plugins.js b/packages/app/electron/build/src/rt/electron-plugins.js deleted file mode 100644 index 0141d0c..0000000 --- a/packages/app/electron/build/src/rt/electron-plugins.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -module.exports = {}; diff --git a/packages/app/electron/build/src/rt/electron-rt.js b/packages/app/electron/build/src/rt/electron-rt.js deleted file mode 100644 index 6260d86..0000000 --- a/packages/app/electron/build/src/rt/electron-rt.js +++ /dev/null @@ -1,68 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const crypto_1 = require("crypto"); -const electron_1 = require("electron"); -const events_1 = require("events"); -//////////////////////////////////////////////////////// -// eslint-disable-next-line @typescript-eslint/no-var-requires -const plugins = require('./electron-plugins'); -const randomId = (length = 5) => (0, crypto_1.randomBytes)(length).toString('hex'); -const contextApi = {}; -Object.keys(plugins).forEach((pluginKey) => { - Object.keys(plugins[pluginKey]) - .filter((className) => className !== 'default') - .forEach((classKey) => { - const functionList = Object.getOwnPropertyNames(plugins[pluginKey][classKey].prototype).filter((v) => v !== 'constructor'); - if (!contextApi[classKey]) { - contextApi[classKey] = {}; - } - functionList.forEach((functionName) => { - if (!contextApi[classKey][functionName]) { - contextApi[classKey][functionName] = (...args) => electron_1.ipcRenderer.invoke(`${classKey}-${functionName}`, ...args); - } - }); - // Events - if (plugins[pluginKey][classKey].prototype instanceof events_1.EventEmitter) { - const listeners = {}; - const listenersOfTypeExist = (type) => !!Object.values(listeners).find((listenerObj) => listenerObj.type === type); - Object.assign(contextApi[classKey], { - addListener(type, callback) { - const id = randomId(); - // Deduplicate events - if (!listenersOfTypeExist(type)) { - electron_1.ipcRenderer.send(`event-add-${classKey}`, type); - } - const eventHandler = (_, ...args) => callback(...args); - electron_1.ipcRenderer.addListener(`event-${classKey}-${type}`, eventHandler); - listeners[id] = { type, listener: eventHandler }; - return id; - }, - removeListener(id) { - if (!listeners[id]) { - throw new Error('Invalid id'); - } - const { type, listener } = listeners[id]; - electron_1.ipcRenderer.removeListener(`event-${classKey}-${type}`, listener); - delete listeners[id]; - if (!listenersOfTypeExist(type)) { - electron_1.ipcRenderer.send(`event-remove-${classKey}-${type}`); - } - }, - removeAllListeners(type) { - Object.entries(listeners).forEach(([id, listenerObj]) => { - if (!type || listenerObj.type === type) { - electron_1.ipcRenderer.removeListener(`event-${classKey}-${listenerObj.type}`, listenerObj.listener); - electron_1.ipcRenderer.send(`event-remove-${classKey}-${listenerObj.type}`); - delete listeners[id]; - } - }); - }, - }); - } - }); -}); -electron_1.contextBridge.exposeInMainWorld('CapacitorCustomPlatform', { - name: 'electron', - plugins: contextApi, -}); -//////////////////////////////////////////////////////// diff --git a/packages/app/electron/build/src/setup.js b/packages/app/electron/build/src/setup.js deleted file mode 100644 index f87f55a..0000000 --- a/packages/app/electron/build/src/setup.js +++ /dev/null @@ -1,205 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.setupContentSecurityPolicy = exports.ElectronCapacitorApp = exports.setupReloadWatcher = void 0; -const tslib_1 = require("tslib"); -const electron_1 = require("@capacitor-community/electron"); -const chokidar_1 = tslib_1.__importDefault(require("chokidar")); -const electron_2 = require("electron"); -const electron_is_dev_1 = tslib_1.__importDefault(require("electron-is-dev")); -const electron_serve_1 = tslib_1.__importDefault(require("electron-serve")); -const electron_window_state_1 = tslib_1.__importDefault(require("electron-window-state")); -const path_1 = require("path"); -// Define components for a watcher to detect when the webapp is changed so we can reload in Dev mode. -const reloadWatcher = { - debouncer: null, - ready: false, - watcher: null, -}; -function setupReloadWatcher(electronCapacitorApp) { - reloadWatcher.watcher = chokidar_1.default - .watch((0, path_1.join)(electron_2.app.getAppPath(), 'app'), { - ignored: /[/\\]\./, - persistent: true, - }) - .on('ready', () => { - reloadWatcher.ready = true; - }) - .on('all', (_event, _path) => { - if (reloadWatcher.ready) { - clearTimeout(reloadWatcher.debouncer); - reloadWatcher.debouncer = setTimeout(async () => { - electronCapacitorApp.getMainWindow().webContents.reload(); - reloadWatcher.ready = false; - clearTimeout(reloadWatcher.debouncer); - reloadWatcher.debouncer = null; - reloadWatcher.watcher = null; - setupReloadWatcher(electronCapacitorApp); - }, 1500); - } - }); -} -exports.setupReloadWatcher = setupReloadWatcher; -// Define our class to manage our app. -class ElectronCapacitorApp { - constructor(capacitorFileConfig, trayMenuTemplate, appMenuBarMenuTemplate) { - var _a, _b; - this.MainWindow = null; - this.SplashScreen = null; - this.TrayIcon = null; - this.TrayMenuTemplate = [ - new electron_2.MenuItem({ label: 'Quit App', role: 'quit' }), - ]; - this.AppMenuBarMenuTemplate = [ - { role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' }, - { role: 'viewMenu' }, - ]; - this.CapacitorFileConfig = capacitorFileConfig; - this.customScheme = (_b = (_a = this.CapacitorFileConfig.electron) === null || _a === void 0 ? void 0 : _a.customUrlScheme) !== null && _b !== void 0 ? _b : 'capacitor-electron'; - if (trayMenuTemplate) { - this.TrayMenuTemplate = trayMenuTemplate; - } - if (appMenuBarMenuTemplate) { - this.AppMenuBarMenuTemplate = appMenuBarMenuTemplate; - } - // Setup our web app loader, this lets us load apps like react, vue, and angular without changing their build chains. - this.loadWebApp = (0, electron_serve_1.default)({ - directory: (0, path_1.join)(electron_2.app.getAppPath(), 'app'), - scheme: this.customScheme, - }); - } - // Helper function to load in the app. - async loadMainWindow(thisRef) { - await thisRef.loadWebApp(thisRef.MainWindow); - } - // Expose the mainWindow ref for use outside of the class. - getMainWindow() { - return this.MainWindow; - } - getCustomURLScheme() { - return this.customScheme; - } - async init() { - var _a, _b, _c, _d; - const icon = electron_2.nativeImage.createFromPath((0, path_1.join)(electron_2.app.getAppPath(), 'assets', process.platform === 'win32' ? 'appIcon.ico' : 'appIcon.png')); - this.mainWindowState = (0, electron_window_state_1.default)({ - defaultWidth: 1000, - defaultHeight: 800, - }); - // Setup preload script path and construct our main window. - const preloadPath = (0, path_1.join)(electron_2.app.getAppPath(), 'build', 'src', 'preload.js'); - this.MainWindow = new electron_2.BrowserWindow({ - icon, - show: false, - x: this.mainWindowState.x, - y: this.mainWindowState.y, - width: this.mainWindowState.width, - height: this.mainWindowState.height, - webPreferences: { - nodeIntegration: true, - contextIsolation: true, - // Use preload to inject the electron varriant overrides for capacitor plugins. - // preload: join(app.getAppPath(), "node_modules", "@capacitor-community", "electron", "dist", "runtime", "electron-rt.js"), - preload: preloadPath, - }, - }); - this.mainWindowState.manage(this.MainWindow); - if (this.CapacitorFileConfig.backgroundColor) { - this.MainWindow.setBackgroundColor(this.CapacitorFileConfig.electron.backgroundColor); - } - // If we close the main window with the splashscreen enabled we need to destory the ref. - this.MainWindow.on('closed', () => { - var _a; - if (((_a = this.SplashScreen) === null || _a === void 0 ? void 0 : _a.getSplashWindow()) && !this.SplashScreen.getSplashWindow().isDestroyed()) { - this.SplashScreen.getSplashWindow().close(); - } - }); - // When the tray icon is enabled, setup the options. - if ((_a = this.CapacitorFileConfig.electron) === null || _a === void 0 ? void 0 : _a.trayIconAndMenuEnabled) { - this.TrayIcon = new electron_2.Tray(icon); - this.TrayIcon.on('double-click', () => { - if (this.MainWindow) { - if (this.MainWindow.isVisible()) { - this.MainWindow.hide(); - } - else { - this.MainWindow.show(); - this.MainWindow.focus(); - } - } - }); - this.TrayIcon.on('click', () => { - if (this.MainWindow) { - if (this.MainWindow.isVisible()) { - this.MainWindow.hide(); - } - else { - this.MainWindow.show(); - this.MainWindow.focus(); - } - } - }); - this.TrayIcon.setToolTip(electron_2.app.getName()); - this.TrayIcon.setContextMenu(electron_2.Menu.buildFromTemplate(this.TrayMenuTemplate)); - } - // Setup the main manu bar at the top of our window. - electron_2.Menu.setApplicationMenu(electron_2.Menu.buildFromTemplate(this.AppMenuBarMenuTemplate)); - // If the splashscreen is enabled, show it first while the main window loads then switch it out for the main window, or just load the main window from the start. - if ((_b = this.CapacitorFileConfig.electron) === null || _b === void 0 ? void 0 : _b.splashScreenEnabled) { - this.SplashScreen = new electron_1.CapacitorSplashScreen({ - imageFilePath: (0, path_1.join)(electron_2.app.getAppPath(), 'assets', (_d = (_c = this.CapacitorFileConfig.electron) === null || _c === void 0 ? void 0 : _c.splashScreenImageName) !== null && _d !== void 0 ? _d : 'splash.png'), - windowWidth: 400, - windowHeight: 400, - }); - this.SplashScreen.init(this.loadMainWindow, this); - } - else { - this.loadMainWindow(this); - } - // Security - this.MainWindow.webContents.setWindowOpenHandler((details) => { - if (!details.url.includes(this.customScheme)) { - return { action: 'deny' }; - } - else { - return { action: 'allow' }; - } - }); - this.MainWindow.webContents.on('will-navigate', (event, _newURL) => { - if (!this.MainWindow.webContents.getURL().includes(this.customScheme)) { - event.preventDefault(); - } - }); - // Link electron plugins into the system. - (0, electron_1.setupCapacitorElectronPlugins)(); - // When the web app is loaded we hide the splashscreen if needed and show the mainwindow. - this.MainWindow.webContents.on('dom-ready', () => { - var _a, _b; - if ((_a = this.CapacitorFileConfig.electron) === null || _a === void 0 ? void 0 : _a.splashScreenEnabled) { - this.SplashScreen.getSplashWindow().hide(); - } - if (!((_b = this.CapacitorFileConfig.electron) === null || _b === void 0 ? void 0 : _b.hideMainWindowOnLaunch)) { - this.MainWindow.show(); - } - setTimeout(() => { - if (electron_is_dev_1.default) { - // this.MainWindow.webContents.openDevTools(); - } - electron_1.CapElectronEventEmitter.emit('CAPELECTRON_DeeplinkListenerInitialized', ''); - }, 400); - }); - } -} -exports.ElectronCapacitorApp = ElectronCapacitorApp; -// Set a CSP up for our application based on the custom scheme -function setupContentSecurityPolicy(customScheme) { - electron_2.session.defaultSession.webRequest.onHeadersReceived((details, callback) => { - callback({ - responseHeaders: Object.assign(Object.assign({}, details.responseHeaders), { 'Content-Security-Policy': [ - electron_is_dev_1.default - ? `default-src ${customScheme}://* 'unsafe-inline' https://sequenced.ottegi.com devtools://* 'unsafe-eval' data:` - : `default-src ${customScheme}://* 'unsafe-inline' https://sequenced.ottegi.com data:`, - ] }), - }); - }); -} -exports.setupContentSecurityPolicy = setupContentSecurityPolicy; diff --git a/packages/app/electron/build/src/sign.js b/packages/app/electron/build/src/sign.js deleted file mode 100644 index c3d220d..0000000 --- a/packages/app/electron/build/src/sign.js +++ /dev/null @@ -1,5 +0,0 @@ -const { signAsync } = require('@electron/osx-sign'); -signAsync({ - app: '../electron/release/mas-universal/Sequenced.app', - provisioningProfile: '../build/Sequenced.appembedded.provisionprofile' -}); diff --git a/packages/app/electron/capacitor.config.ts b/packages/app/electron/capacitor.config.ts deleted file mode 100644 index 7224d54..0000000 --- a/packages/app/electron/capacitor.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { CapacitorElectronConfig } from "@capacitor-community/electron"; - -const config = { - appId: "com.ottegi.sequenced-app", - appName: "Sequenced", - webDir: "dist", - server: { - androidScheme: "https", - }, -}; - -const configElectron = {}; - -const configGlobal: CapacitorElectronConfig = { - ...config, - electron: configElectron, - plugins: { - LocalNotifications: { - smallIcon: "icon", - iconColor: "#307acf", - sound: "beep.wav", - }, - }, -}; - -export default configGlobal; diff --git a/packages/app/electron/electron-builder.config.json b/packages/app/electron/electron-builder.config.json deleted file mode 100644 index 3512ba3..0000000 --- a/packages/app/electron/electron-builder.config.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "appId": "com.ottegi.sequenced-app", - "directories": { - "buildResources": "resources", - "output": "release" - }, - "publish": null, - "files": ["assets/**/*", "build/**/*", "capacitor.config.*", "app/**/*"], - "pkg": { - "identity": null - }, - "mas": { - "hardenedRuntime": true, - "provisioningProfile": "build/embedded.provisionprofile", - "category": "public.app-category.utilities", - "entitlements": "build/entitlements.mas.plist", - "entitlementsInherit": "build/entitlements.mas.inherit.plist", - "gatekeeperAssess": true, - "asarUnpack": [] - }, - "nsis": { - "allowElevation": true, - "oneClick": false, - "allowToChangeInstallationDirectory": true - }, - "win": { - "target": "nsis", - "icon": "assets/icon.png" - }, - "mac": { - "identity": null, - "type": "distribution", - "target": [ - { "target": "mas", "arch": ["universal"] } - ], - "artifactName": "${productName}.${ext}", - "category": "public.app-category.utilities", - "entitlements": "build/entitlements.mas.plist", - "entitlementsInherit": "build/entitlements.mas.inherit.plist", - "icon": "assets/icon.png", - "hardenedRuntime": true, - "gatekeeperAssess": true, - "notarize": { - "teamId": "B9UMLF8BH7" - } - } -} diff --git a/packages/app/electron/forge.config.js b/packages/app/electron/forge.config.js deleted file mode 100644 index fa4a113..0000000 --- a/packages/app/electron/forge.config.js +++ /dev/null @@ -1,44 +0,0 @@ -const { FusesPlugin } = require('@electron-forge/plugin-fuses'); -const { FuseV1Options, FuseVersion } = require('@electron/fuses'); - -module.exports = { - packagerConfig: { - asar: true, - }, - rebuildConfig: {}, - makers: [ - { - name: '@electron-forge/maker-squirrel', - config: {}, - }, - { - name: '@electron-forge/maker-zip', - platforms: ['darwin'], - }, - { - name: '@electron-forge/maker-deb', - config: {}, - }, - { - name: '@electron-forge/maker-rpm', - config: {}, - }, - ], - plugins: [ - { - name: '@electron-forge/plugin-auto-unpack-natives', - config: {}, - }, - // Fuses are used to enable/disable various Electron functionality - // at package time, before code signing the application - new FusesPlugin({ - version: FuseVersion.V1, - [FuseV1Options.RunAsNode]: false, - [FuseV1Options.EnableCookieEncryption]: true, - [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, - [FuseV1Options.EnableNodeCliInspectArguments]: false, - [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true, - [FuseV1Options.OnlyLoadAppFromAsar]: true, - }), - ], -}; diff --git a/packages/app/electron/live-runner.js b/packages/app/electron/live-runner.js deleted file mode 100644 index 84c3ea7..0000000 --- a/packages/app/electron/live-runner.js +++ /dev/null @@ -1,75 +0,0 @@ -/* eslint-disable no-undef */ -/* eslint-disable @typescript-eslint/no-var-requires */ -const cp = require('child_process'); -const chokidar = require('chokidar'); -const electron = require('electron'); - -let child = null; -const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'; -const reloadWatcher = { - debouncer: null, - ready: false, - watcher: null, - restarting: false, -}; - -///* -function runBuild() { - return new Promise((resolve, _reject) => { - let tempChild = cp.spawn(npmCmd, ['run', 'build']); - tempChild.once('exit', () => { - resolve(); - }); - tempChild.stdout.pipe(process.stdout); - }); -} -//*/ - -async function spawnElectron() { - if (child !== null) { - child.stdin.pause(); - child.kill(); - child = null; - await runBuild(); - } - child = cp.spawn(electron, ['--inspect=5858', './']); - child.on('exit', () => { - if (!reloadWatcher.restarting) { - process.exit(0); - } - }); - child.stdout.pipe(process.stdout); -} - -function setupReloadWatcher() { - reloadWatcher.watcher = chokidar - .watch('./src/**/*', { - ignored: /[/\\]\./, - persistent: true, - }) - .on('ready', () => { - reloadWatcher.ready = true; - }) - .on('all', (_event, _path) => { - if (reloadWatcher.ready) { - clearTimeout(reloadWatcher.debouncer); - reloadWatcher.debouncer = setTimeout(async () => { - console.log('Restarting'); - reloadWatcher.restarting = true; - await spawnElectron(); - reloadWatcher.restarting = false; - reloadWatcher.ready = false; - clearTimeout(reloadWatcher.debouncer); - reloadWatcher.debouncer = null; - reloadWatcher.watcher = null; - setupReloadWatcher(); - }, 500); - } - }); -} - -(async () => { - await runBuild(); - await spawnElectron(); - setupReloadWatcher(); -})(); diff --git a/packages/app/electron/package.json b/packages/app/electron/package.json deleted file mode 100644 index 485351b..0000000 --- a/packages/app/electron/package.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "name": "Sequenced", - "version": "1.0.9", - "description": "", - "author": { - "name": "", - "email": "" - }, - "repository": { - "type": "git", - "url": "https://github.com/DoctorNovus/sequenced" - }, - "license": "MIT", - "main": "build/src/index.js", - "scripts": { - "postinstall": "electron-builder install-app-deps", - "build": "tsc && electron-rebuild", - "rebuild": "bash build/resignAndPackage.sh", - "electron:start-live": "node ./live-runner.js", - "electron:start": "npm run build && electron --inspect=5858 ./", - "electron:pack": "npm run build && electron-builder build --dir -c ./electron-builder.config.json", - "electron:make": "npm run build && electron-builder build -c ./electron-builder.config.json -p always --mac --win", - "electron:make:mac": "npm run build && electron-builder build -c ./electron-builder.config.json -p always --mac", - "electron:make:windows": "npm run build && electron-builder build -c ./electron-builder.config.json -p always --win", - "sign:dev": "electron-osx-sign release/Sequenced.pkg --provisioning-profile=build/embedded.provisionprofile --identity='Apple Development'", - "sign": "electron-osx-sign release/Sequenced.pkg --identity='Apple Distribution'", - "start": "electron-forge start", - "package": "electron-forge package", - "make": "electron-forge make" - }, - "dependencies": { - "@capacitor-community/electron": "^5.0.0", - "@types/node": "^16.18.71", - "chokidar": "~3.5.3", - "electron-is-dev": "~2.0.0", - "electron-serve": "~1.1.0", - "electron-squirrel-startup": "^1.0.1", - "electron-unhandled": "~4.0.1", - "electron-updater": "^5.3.0", - "electron-window-state": "^5.0.3" - }, - "devDependencies": { - "@electron-forge/cli": "^7.4.0", - "@electron-forge/maker-deb": "^7.4.0", - "@electron-forge/maker-rpm": "^7.4.0", - "@electron-forge/maker-squirrel": "^7.4.0", - "@electron-forge/maker-zip": "^7.4.0", - "@electron-forge/plugin-auto-unpack-natives": "^7.4.0", - "@electron-forge/plugin-fuses": "^7.4.0", - "@electron/fuses": "^1.8.0", - "@electron/notarize": "^2.3.2", - "electron": "^26.2.2", - "electron-rebuild": "^3.2.9", - "typescript": "^5.0.4" - }, - "keywords": [ - "capacitor", - "electron" - ] -} diff --git a/packages/app/electron/resources/icon.png b/packages/app/electron/resources/icon.png deleted file mode 100644 index 6c11e78..0000000 Binary files a/packages/app/electron/resources/icon.png and /dev/null differ diff --git a/packages/app/electron/src/index.ts b/packages/app/electron/src/index.ts deleted file mode 100644 index fc5db4b..0000000 --- a/packages/app/electron/src/index.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { CapacitorElectronConfig } from '@capacitor-community/electron'; -import { getCapacitorElectronConfig, setupElectronDeepLinking } from '@capacitor-community/electron'; -import type { MenuItemConstructorOptions } from 'electron'; -import { app, MenuItem } from 'electron'; -import electronIsDev from 'electron-is-dev'; -import unhandled from 'electron-unhandled'; - -import { ElectronCapacitorApp, setupContentSecurityPolicy, setupReloadWatcher } from './setup'; - -// Graceful handling of unhandled errors. -unhandled(); - -// Define our menu templates (these are optional) -const trayMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [new MenuItem({ label: 'Quit App', role: 'quit' })]; -const appMenuBarMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [ - { role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' }, - { role: 'viewMenu' }, -]; - -// Get Config options from capacitor.config -const capacitorFileConfig: CapacitorElectronConfig = getCapacitorElectronConfig(); - -// Initialize our app. You can pass menu templates into the app here. -// const myCapacitorApp = new ElectronCapacitorApp(capacitorFileConfig); -const myCapacitorApp = new ElectronCapacitorApp(capacitorFileConfig, trayMenuTemplate, appMenuBarMenuTemplate); - -// If deeplinking is enabled then we will set it up here. -if (capacitorFileConfig.electron?.deepLinkingEnabled) { - setupElectronDeepLinking(myCapacitorApp, { - customProtocol: capacitorFileConfig.electron.deepLinkingCustomProtocol ?? 'mycapacitorapp', - }); -} - -// If we are in Dev mode, use the file watcher components. -if (electronIsDev) { - setupReloadWatcher(myCapacitorApp); -} - -// Run Application -(async () => { - // Wait for electron app to be ready. - await app.whenReady(); - // Security - Set Content-Security-Policy based on whether or not we are in dev mode. - setupContentSecurityPolicy(myCapacitorApp.getCustomURLScheme()); - // Initialize our app, build windows, and load content. - await myCapacitorApp.init(); - // Check for updates if we are in a packaged app. -})(); - -// Handle when all of our windows are close (platforms have their own expectations). -app.on('window-all-closed', function () { - // On OS X it is common for applications and their menu bar - // to stay active until the user quits explicitly with Cmd + Q - if (process.platform !== 'darwin') { - app.quit(); - } -}); - -// When the dock icon is clicked. -app.on('activate', async function () { - // On OS X it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (myCapacitorApp.getMainWindow().isDestroyed()) { - await myCapacitorApp.init(); - } -}); - -// Place all ipc or other electron api calls and custom functionality under this line diff --git a/packages/app/electron/src/preload.ts b/packages/app/electron/src/preload.ts deleted file mode 100644 index c817d3b..0000000 --- a/packages/app/electron/src/preload.ts +++ /dev/null @@ -1,4 +0,0 @@ -require('./rt/electron-rt'); -////////////////////////////// -// User Defined Preload scripts below -console.log('User Preload!'); diff --git a/packages/app/electron/src/rt/electron-plugins.js b/packages/app/electron/src/rt/electron-plugins.js deleted file mode 100644 index b33b282..0000000 --- a/packages/app/electron/src/rt/electron-plugins.js +++ /dev/null @@ -1,4 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ - -module.exports = { -} \ No newline at end of file diff --git a/packages/app/electron/src/rt/electron-rt.ts b/packages/app/electron/src/rt/electron-rt.ts deleted file mode 100644 index 55d67c3..0000000 --- a/packages/app/electron/src/rt/electron-rt.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { randomBytes } from 'crypto'; -import { ipcRenderer, contextBridge } from 'electron'; -import { EventEmitter } from 'events'; - -//////////////////////////////////////////////////////// -// eslint-disable-next-line @typescript-eslint/no-var-requires -const plugins = require('./electron-plugins'); - -const randomId = (length = 5) => randomBytes(length).toString('hex'); - -const contextApi: { - [plugin: string]: { [functionName: string]: () => Promise }; -} = {}; - -Object.keys(plugins).forEach((pluginKey) => { - Object.keys(plugins[pluginKey]) - .filter((className) => className !== 'default') - .forEach((classKey) => { - const functionList = Object.getOwnPropertyNames(plugins[pluginKey][classKey].prototype).filter( - (v) => v !== 'constructor' - ); - - if (!contextApi[classKey]) { - contextApi[classKey] = {}; - } - - functionList.forEach((functionName) => { - if (!contextApi[classKey][functionName]) { - contextApi[classKey][functionName] = (...args) => ipcRenderer.invoke(`${classKey}-${functionName}`, ...args); - } - }); - - // Events - if (plugins[pluginKey][classKey].prototype instanceof EventEmitter) { - const listeners: { [key: string]: { type: string; listener: (...args: any[]) => void } } = {}; - const listenersOfTypeExist = (type) => - !!Object.values(listeners).find((listenerObj) => listenerObj.type === type); - - Object.assign(contextApi[classKey], { - addListener(type: string, callback: (...args) => void) { - const id = randomId(); - - // Deduplicate events - if (!listenersOfTypeExist(type)) { - ipcRenderer.send(`event-add-${classKey}`, type); - } - - const eventHandler = (_, ...args) => callback(...args); - - ipcRenderer.addListener(`event-${classKey}-${type}`, eventHandler); - listeners[id] = { type, listener: eventHandler }; - - return id; - }, - removeListener(id: string) { - if (!listeners[id]) { - throw new Error('Invalid id'); - } - - const { type, listener } = listeners[id]; - - ipcRenderer.removeListener(`event-${classKey}-${type}`, listener); - - delete listeners[id]; - - if (!listenersOfTypeExist(type)) { - ipcRenderer.send(`event-remove-${classKey}-${type}`); - } - }, - removeAllListeners(type: string) { - Object.entries(listeners).forEach(([id, listenerObj]) => { - if (!type || listenerObj.type === type) { - ipcRenderer.removeListener(`event-${classKey}-${listenerObj.type}`, listenerObj.listener); - ipcRenderer.send(`event-remove-${classKey}-${listenerObj.type}`); - delete listeners[id]; - } - }); - }, - }); - } - }); -}); - -contextBridge.exposeInMainWorld('CapacitorCustomPlatform', { - name: 'electron', - plugins: contextApi, -}); -//////////////////////////////////////////////////////// diff --git a/packages/app/electron/src/setup.ts b/packages/app/electron/src/setup.ts deleted file mode 100644 index 8819977..0000000 --- a/packages/app/electron/src/setup.ts +++ /dev/null @@ -1,233 +0,0 @@ -import type { CapacitorElectronConfig } from '@capacitor-community/electron'; -import { - CapElectronEventEmitter, - CapacitorSplashScreen, - setupCapacitorElectronPlugins, -} from '@capacitor-community/electron'; -import chokidar from 'chokidar'; -import type { MenuItemConstructorOptions } from 'electron'; -import { app, BrowserWindow, Menu, MenuItem, nativeImage, Tray, session } from 'electron'; -import electronIsDev from 'electron-is-dev'; -import electronServe from 'electron-serve'; -import windowStateKeeper from 'electron-window-state'; -import { join } from 'path'; - -// Define components for a watcher to detect when the webapp is changed so we can reload in Dev mode. -const reloadWatcher = { - debouncer: null, - ready: false, - watcher: null, -}; -export function setupReloadWatcher(electronCapacitorApp: ElectronCapacitorApp): void { - reloadWatcher.watcher = chokidar - .watch(join(app.getAppPath(), 'app'), { - ignored: /[/\\]\./, - persistent: true, - }) - .on('ready', () => { - reloadWatcher.ready = true; - }) - .on('all', (_event, _path) => { - if (reloadWatcher.ready) { - clearTimeout(reloadWatcher.debouncer); - reloadWatcher.debouncer = setTimeout(async () => { - electronCapacitorApp.getMainWindow().webContents.reload(); - reloadWatcher.ready = false; - clearTimeout(reloadWatcher.debouncer); - reloadWatcher.debouncer = null; - reloadWatcher.watcher = null; - setupReloadWatcher(electronCapacitorApp); - }, 1500); - } - }); -} - -// Define our class to manage our app. -export class ElectronCapacitorApp { - private MainWindow: BrowserWindow | null = null; - private SplashScreen: CapacitorSplashScreen | null = null; - private TrayIcon: Tray | null = null; - private CapacitorFileConfig: CapacitorElectronConfig; - private TrayMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [ - new MenuItem({ label: 'Quit App', role: 'quit' }), - ]; - private AppMenuBarMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [ - { role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' }, - { role: 'viewMenu' }, - ]; - private mainWindowState; - private loadWebApp; - private customScheme: string; - - constructor( - capacitorFileConfig: CapacitorElectronConfig, - trayMenuTemplate?: (MenuItemConstructorOptions | MenuItem)[], - appMenuBarMenuTemplate?: (MenuItemConstructorOptions | MenuItem)[] - ) { - this.CapacitorFileConfig = capacitorFileConfig; - - this.customScheme = this.CapacitorFileConfig.electron?.customUrlScheme ?? 'capacitor-electron'; - - if (trayMenuTemplate) { - this.TrayMenuTemplate = trayMenuTemplate; - } - - if (appMenuBarMenuTemplate) { - this.AppMenuBarMenuTemplate = appMenuBarMenuTemplate; - } - - // Setup our web app loader, this lets us load apps like react, vue, and angular without changing their build chains. - this.loadWebApp = electronServe({ - directory: join(app.getAppPath(), 'app'), - scheme: this.customScheme, - }); - } - - // Helper function to load in the app. - private async loadMainWindow(thisRef: any) { - await thisRef.loadWebApp(thisRef.MainWindow); - } - - // Expose the mainWindow ref for use outside of the class. - getMainWindow(): BrowserWindow { - return this.MainWindow; - } - - getCustomURLScheme(): string { - return this.customScheme; - } - - async init(): Promise { - const icon = nativeImage.createFromPath( - join(app.getAppPath(), 'assets', process.platform === 'win32' ? 'appIcon.ico' : 'appIcon.png') - ); - this.mainWindowState = windowStateKeeper({ - defaultWidth: 1000, - defaultHeight: 800, - }); - // Setup preload script path and construct our main window. - const preloadPath = join(app.getAppPath(), 'build', 'src', 'preload.js'); - this.MainWindow = new BrowserWindow({ - icon, - show: false, - x: this.mainWindowState.x, - y: this.mainWindowState.y, - width: this.mainWindowState.width, - height: this.mainWindowState.height, - webPreferences: { - nodeIntegration: true, - contextIsolation: true, - // Use preload to inject the electron varriant overrides for capacitor plugins. - // preload: join(app.getAppPath(), "node_modules", "@capacitor-community", "electron", "dist", "runtime", "electron-rt.js"), - preload: preloadPath, - }, - }); - this.mainWindowState.manage(this.MainWindow); - - if (this.CapacitorFileConfig.backgroundColor) { - this.MainWindow.setBackgroundColor(this.CapacitorFileConfig.electron.backgroundColor); - } - - // If we close the main window with the splashscreen enabled we need to destory the ref. - this.MainWindow.on('closed', () => { - if (this.SplashScreen?.getSplashWindow() && !this.SplashScreen.getSplashWindow().isDestroyed()) { - this.SplashScreen.getSplashWindow().close(); - } - }); - - // When the tray icon is enabled, setup the options. - if (this.CapacitorFileConfig.electron?.trayIconAndMenuEnabled) { - this.TrayIcon = new Tray(icon); - this.TrayIcon.on('double-click', () => { - if (this.MainWindow) { - if (this.MainWindow.isVisible()) { - this.MainWindow.hide(); - } else { - this.MainWindow.show(); - this.MainWindow.focus(); - } - } - }); - this.TrayIcon.on('click', () => { - if (this.MainWindow) { - if (this.MainWindow.isVisible()) { - this.MainWindow.hide(); - } else { - this.MainWindow.show(); - this.MainWindow.focus(); - } - } - }); - this.TrayIcon.setToolTip(app.getName()); - this.TrayIcon.setContextMenu(Menu.buildFromTemplate(this.TrayMenuTemplate)); - } - - // Setup the main manu bar at the top of our window. - Menu.setApplicationMenu(Menu.buildFromTemplate(this.AppMenuBarMenuTemplate)); - - // If the splashscreen is enabled, show it first while the main window loads then switch it out for the main window, or just load the main window from the start. - if (this.CapacitorFileConfig.electron?.splashScreenEnabled) { - this.SplashScreen = new CapacitorSplashScreen({ - imageFilePath: join( - app.getAppPath(), - 'assets', - this.CapacitorFileConfig.electron?.splashScreenImageName ?? 'splash.png' - ), - windowWidth: 400, - windowHeight: 400, - }); - this.SplashScreen.init(this.loadMainWindow, this); - } else { - this.loadMainWindow(this); - } - - // Security - this.MainWindow.webContents.setWindowOpenHandler((details) => { - if (!details.url.includes(this.customScheme)) { - return { action: 'deny' }; - } else { - return { action: 'allow' }; - } - }); - this.MainWindow.webContents.on('will-navigate', (event, _newURL) => { - if (!this.MainWindow.webContents.getURL().includes(this.customScheme)) { - event.preventDefault(); - } - }); - - // Link electron plugins into the system. - setupCapacitorElectronPlugins(); - - // When the web app is loaded we hide the splashscreen if needed and show the mainwindow. - this.MainWindow.webContents.on('dom-ready', () => { - if (this.CapacitorFileConfig.electron?.splashScreenEnabled) { - this.SplashScreen.getSplashWindow().hide(); - } - if (!this.CapacitorFileConfig.electron?.hideMainWindowOnLaunch) { - this.MainWindow.show(); - } - setTimeout(() => { - if (electronIsDev) { - // this.MainWindow.webContents.openDevTools(); - } - CapElectronEventEmitter.emit('CAPELECTRON_DeeplinkListenerInitialized', ''); - }, 400); - }); - } -} - -// Set a CSP up for our application based on the custom scheme -export function setupContentSecurityPolicy(customScheme: string): void { - session.defaultSession.webRequest.onHeadersReceived((details, callback) => { - callback({ - responseHeaders: { - ...details.responseHeaders, - 'Content-Security-Policy': [ - electronIsDev - ? `default-src ${customScheme}://* 'unsafe-inline' https://sequenced.ottegi.com devtools://* 'unsafe-eval' data:` - : `default-src ${customScheme}://* 'unsafe-inline' https://sequenced.ottegi.com data:`, - ], - }, - }); - }); -} diff --git a/packages/app/electron/src/sign.ts b/packages/app/electron/src/sign.ts deleted file mode 100644 index 831c3d4..0000000 --- a/packages/app/electron/src/sign.ts +++ /dev/null @@ -1,6 +0,0 @@ -const { signAsync } = require('@electron/osx-sign') - -signAsync({ - app: '../electron/release/mas-universal/Sequenced.app', - provisioningProfile: '../build/Sequenced.appembedded.provisionprofile' -}); \ No newline at end of file diff --git a/packages/app/electron/tsconfig.json b/packages/app/electron/tsconfig.json deleted file mode 100644 index 508f26c..0000000 --- a/packages/app/electron/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compileOnSave": true, - "include": ["./src/**/*", "./capacitor.config.ts", "./capacitor.config.js"], - "compilerOptions": { - "outDir": "./build", - "importHelpers": true, - "target": "ES2017", - "module": "CommonJS", - "moduleResolution": "node", - "esModuleInterop": true, - "typeRoots": ["./node_modules/@types"], - "allowJs": true, - "rootDir": ".", - "skipLibCheck": true - } -} diff --git a/packages/app/forge.config.js b/packages/app/forge.config.js deleted file mode 100644 index fa4a113..0000000 --- a/packages/app/forge.config.js +++ /dev/null @@ -1,44 +0,0 @@ -const { FusesPlugin } = require('@electron-forge/plugin-fuses'); -const { FuseV1Options, FuseVersion } = require('@electron/fuses'); - -module.exports = { - packagerConfig: { - asar: true, - }, - rebuildConfig: {}, - makers: [ - { - name: '@electron-forge/maker-squirrel', - config: {}, - }, - { - name: '@electron-forge/maker-zip', - platforms: ['darwin'], - }, - { - name: '@electron-forge/maker-deb', - config: {}, - }, - { - name: '@electron-forge/maker-rpm', - config: {}, - }, - ], - plugins: [ - { - name: '@electron-forge/plugin-auto-unpack-natives', - config: {}, - }, - // Fuses are used to enable/disable various Electron functionality - // at package time, before code signing the application - new FusesPlugin({ - version: FuseVersion.V1, - [FuseV1Options.RunAsNode]: false, - [FuseV1Options.EnableCookieEncryption]: true, - [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, - [FuseV1Options.EnableNodeCliInspectArguments]: false, - [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true, - [FuseV1Options.OnlyLoadAppFromAsar]: true, - }), - ], -}; diff --git a/packages/app/index.html b/packages/app/index.html index 81c8ac0..3555aac 100644 --- a/packages/app/index.html +++ b/packages/app/index.html @@ -10,12 +10,12 @@ - + - + @@ -46,4 +46,4 @@ - \ No newline at end of file + diff --git a/packages/app/ios/App/App.xcodeproj/project.pbxproj b/packages/app/ios/App/App.xcodeproj/project.pbxproj index e7ee4ef..2c9cdfe 100644 --- a/packages/app/ios/App/App.xcodeproj/project.pbxproj +++ b/packages/app/ios/App/App.xcodeproj/project.pbxproj @@ -112,7 +112,6 @@ 504EC3011FED79650016851F /* Frameworks */, 504EC3021FED79650016851F /* Resources */, D8D5881C74F8CCF32649C6BC /* [CP] Embed Pods Frameworks */, - 0A46007D5F0BB975CAE566E4 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -174,21 +173,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 0A46007D5F0BB975CAE566E4 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; 54623D73B75D61969A5B5904 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -382,8 +366,8 @@ CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = B9UMLF8BH7; + CURRENT_PROJECT_VERSION = 3; + DEVELOPMENT_TEAM = 6E2ZS4K5CT; ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = App/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Sequenced; @@ -395,7 +379,7 @@ ); MARKETING_VERSION = 2.0.1; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; - PRODUCT_BUNDLE_IDENTIFIER = "com.ottegi.sequenced-app"; + PRODUCT_BUNDLE_IDENTIFIER = "com.ottegi-dev.sequenced-app"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; @@ -417,8 +401,8 @@ CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = B9UMLF8BH7; + CURRENT_PROJECT_VERSION = 3; + DEVELOPMENT_TEAM = 6E2ZS4K5CT; ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = App/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Sequenced; @@ -429,7 +413,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 2.0.1; - PRODUCT_BUNDLE_IDENTIFIER = "com.ottegi.sequenced-app"; + PRODUCT_BUNDLE_IDENTIFIER = "com.ottegi-dev.sequenced-app"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; diff --git a/packages/app/ios/App/App/Info.plist b/packages/app/ios/App/App/Info.plist index be1bc14..38d2b03 100644 --- a/packages/app/ios/App/App/Info.plist +++ b/packages/app/ios/App/App/Info.plist @@ -63,7 +63,7 @@ UIRequiresFullScreen UIStatusBarStyle - UIStatusBarStyleDarkContent + UIStatusBarStyleLightContent UISupportedInterfaceOrientations UIInterfaceOrientationPortrait @@ -76,6 +76,6 @@ UIInterfaceOrientationPortraitUpsideDown UIViewControllerBasedStatusBarAppearance - + diff --git a/packages/app/ios/App/Podfile b/packages/app/ios/App/Podfile index c286724..ef33212 100644 --- a/packages/app/ios/App/Podfile +++ b/packages/app/ios/App/Podfile @@ -11,9 +11,6 @@ install! 'cocoapods', :disable_input_output_paths => true def capacitor_pods pod 'Capacitor', :path => '../../../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../../../node_modules/@capacitor/ios' - pod 'CapacitorCommunityAdmob', :path => '../../../../node_modules/@capacitor-community/admob' - pod 'CapacitorCommunitySqlite', :path => '../../../../node_modules/@capacitor-community/sqlite' - pod 'CapacitorFilesystem', :path => '../../../../node_modules/@capacitor/filesystem' pod 'CapacitorLocalNotifications', :path => '../../../../node_modules/@capacitor/local-notifications' pod 'CapacitorPreferences', :path => '../../../../node_modules/@capacitor/preferences' end diff --git a/packages/app/package.json b/packages/app/package.json index bbebc5e..9d6fe53 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -8,7 +8,7 @@ }, "scripts": { "dev": "vite", - "build": "vite build", + "build": "tsc && vite build", "build:full": "npm run build && npm run sync", "sync": "npx cap sync", "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", @@ -25,23 +25,18 @@ "resources": "capacitor-resources -p android,ios" }, "dependencies": { - "@capacitor-community/admob": "^7.0.3", - "@capacitor-community/sqlite": "^7.0.2", "@capacitor/android": "^7.0.0", "@capacitor/core": "^7.0.0", - "@capacitor/filesystem": "^7.0.0", "@capacitor/ios": "^7.0.0", "@capacitor/local-notifications": "^7.0.0", "@capacitor/preferences": "^7.0.0", "@headlessui/react": "^2.0.4", "@heroicons/react": "^2.1.3", "@tanstack/react-query": "^5.40.1", - "@types/node": "^20.14.2", "nanoid": "^5.0.7", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router": "^7.1.1", - "tailwind-merge": "^2.3.0", "vite-plugin-pwa": "^1.0.0" }, "devDependencies": { @@ -56,7 +51,6 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.7", "postcss": "^8.4.38", - "react-router-dom": "^6.23.1", "tailwindcss": "^3.4.4", "typescript": "^5.4.5", "vite": "^5.2.13" diff --git a/packages/app/src/components/calendar/ActiveCalendar.tsx b/packages/app/src/components/calendar/ActiveCalendar.tsx index fcac099..10031ac 100644 --- a/packages/app/src/components/calendar/ActiveCalendar.tsx +++ b/packages/app/src/components/calendar/ActiveCalendar.tsx @@ -13,7 +13,7 @@ export default function ActiveCalendar({ skeleton }: ActiveCalendarProps) { const [appData, setAppData] = useApp(); const shiftWeek = (direction: number) => { - const tempDate = new Date(appData.activeDate); + const tempDate = new Date(appData.activeDate!); tempDate.setDate(tempDate.getDate() + 7 * direction); setAppData({ ...appData, activeDate: tempDate }); }; @@ -76,9 +76,9 @@ export default function ActiveCalendar({ skeleton }: ActiveCalendarProps) { ...appData, }; - const tempYear = activeData[0]; - const tempMonth = activeData[1] - 1; - const tempDay = activeData[2]; + const tempYear = parseInt(activeData[0]); + const tempMonth = parseInt(activeData[1]) - 1; + const tempDay = parseInt(activeData[2]); tempData.activeDate = new Date(); tempData.activeDate.setFullYear(tempYear); @@ -95,11 +95,11 @@ export default function ActiveCalendar({ skeleton }: ActiveCalendarProps) { return 0; }; - const touchstart = (e) => { + const touchstart = (e: React.TouchEvent) => { touchstartX = e.changedTouches[0].screenX; }; - const touchend = (e) => { + const touchend = (e: React.TouchEvent) => { touchendX = e.changedTouches[0].screenX; const direction = checkDirection(); @@ -122,7 +122,7 @@ export default function ActiveCalendar({ skeleton }: ActiveCalendarProps) {
diff --git a/packages/app/src/components/calendar/CalendarArrow.tsx b/packages/app/src/components/calendar/CalendarArrow.tsx index 76093c1..76ae9a2 100644 --- a/packages/app/src/components/calendar/CalendarArrow.tsx +++ b/packages/app/src/components/calendar/CalendarArrow.tsx @@ -1,7 +1,13 @@ import arrow_left from "@/assets/arrow_left.svg"; import arrow_right from "@/assets/arrow_right.svg"; -export default function CalendarArrow({ direction, activeWeek, changeActiveWeek }) { +interface CalendarArrowProps { + direction: string; + activeWeek: number; + changeActiveWeek: (week: number) => void; +} + +export default function CalendarArrow({ direction, activeWeek, changeActiveWeek }: CalendarArrowProps) { let source = arrow_left; if (direction == "right") source = arrow_right; diff --git a/packages/app/src/components/calendar/CalendarItem.tsx b/packages/app/src/components/calendar/CalendarItem.tsx index ab42f20..b208fce 100644 --- a/packages/app/src/components/calendar/CalendarItem.tsx +++ b/packages/app/src/components/calendar/CalendarItem.tsx @@ -12,7 +12,7 @@ export default function CalendarItem({ skeleton, date }: {skeleton?: boolean, da ) } - const changeDate = (date, e) => { + const changeDate = (date: Date) => { let tempData = { ...appData, activeDate: date @@ -23,25 +23,6 @@ export default function CalendarItem({ skeleton, date }: {skeleton?: boolean, da setAppData(tempData); } - function convertDay(num) { - switch (num) { - case 0: - return "Sunday"; - case 1: - return "Monday"; - case 2: - return "Tuesday"; - case 3: - return "Wednesday"; - case 4: - return "Thursday"; - case 5: - return "Friday"; - case 6: - return "Saturday"; - } - } - const today = new Date(); const isToday = today.toDateString() === date.toDateString(); const isActive = appData.activeDate?.toDateString?.() === date.toDateString(); @@ -56,7 +37,7 @@ export default function CalendarItem({ skeleton, date }: {skeleton?: boolean, da return ( ) } diff --git a/packages/app/src/pages/(Home)/HomeAgenda.tsx b/packages/app/src/pages/(Home)/HomeAgenda.tsx index 6a874ae..e5b0c9a 100644 --- a/packages/app/src/pages/(Home)/HomeAgenda.tsx +++ b/packages/app/src/pages/(Home)/HomeAgenda.tsx @@ -1,15 +1,53 @@ -import { useTasksOverdue, useTasksToday, useTasksTomorrow, useTasksWeek } from "@/hooks/tasks"; +import { Task, useTasks } from "@/hooks/tasks"; import DueCapsule from "./DueCapsule"; +import { useNavigate } from "react-router"; +import { occursOnDate, isTaskDone, normalizeDay } from "@/utils/data"; +import { useMemo } from "react"; type AgendaProps = { skeleton?: boolean; }; export default function HomeAgenda({ skeleton }: AgendaProps) { - const dueToday = useTasksToday(); - const dueTomorrow = useTasksTomorrow(); - const dueWeek = useTasksWeek(); - const overdueTasks = useTasksOverdue(); + const navigate = useNavigate(); + const tasks = useTasks(); + + const today = normalizeDay(new Date()); + const tomorrow = normalizeDay(new Date(Date.now() + 24 * 60 * 60 * 1000)); + + const isPendingOnDate = (task: Task, day: Date) => occursOnDate(task, day) && isTaskDone(task, day); + const hasPendingWithinDays = (task: Task, startDay: Date, days: number) => { + for (let i = 0; i < days; i++) { + const check = new Date(startDay); + check.setDate(startDay.getDate() + i); + if (isPendingOnDate(task, check)) return true; + } + return false; + }; + const hasPendingBefore = (task: Task, target: Date) => { + const startDay = normalizeDay(task.date as any); + if (Number.isNaN(startDay.getTime()) || startDay > target) return false; + + const totalDays = Math.floor((target.getTime() - startDay.getTime()) / (24 * 60 * 60 * 1000)); + for (let i = 0; i <= totalDays; i++) { + const check = new Date(startDay); + check.setDate(startDay.getDate() + i); + if (isPendingOnDate(task, check) && check < target) return true; + } + return false; + }; + + const counts = useMemo(() => { + if (!tasks.data) return { today: 0, tomorrow: 0, week: 0, overdue: 0 }; + const base = { today: 0, tomorrow: 0, week: 0, overdue: 0 }; + for (const task of tasks.data) { + if (isPendingOnDate(task, today)) base.today += 1; + if (isPendingOnDate(task, tomorrow)) base.tomorrow += 1; + if (hasPendingWithinDays(task, today, 7)) base.week += 1; + if (hasPendingBefore(task, today)) base.overdue += 1; + } + return base; + }, [tasks.data, today, tomorrow]); if (skeleton) return ( @@ -23,13 +61,9 @@ export default function HomeAgenda({ skeleton }: AgendaProps) {
-
- -
-
- - -
+ + +
Overdue Tasks @@ -49,38 +83,55 @@ export default function HomeAgenda({ skeleton }: AgendaProps) {
-
- { - dueToday.isSuccess && ( - - ) - } -
-
- { - dueTomorrow.isSuccess && ( - - ) - } - { - dueWeek.isSuccess && ( - - ) - } -
+ { + tasks.isSuccess && ( + navigate("/calendar?scope=today&view=week")} + /> + ) + } + { + tasks.isSuccess && ( + navigate("/calendar?scope=tomorrow&view=week")} + /> + ) + } + { + tasks.isSuccess && ( + navigate("/calendar?scope=week&view=week")} + /> + ) + }
0 + role="button" + tabIndex={0} + onClick={() => navigate("/calendar?scope=overdue&view=week")} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + navigate("/calendar?scope=overdue&view=week"); + } + }} + className={`group mt-4 flex items-center justify-between rounded-2xl border px-4 py-3 cursor-pointer transition hover:border-accent-blue/40 hover:ring-1 hover:ring-accent-blue/25 ${ + tasks.isSuccess && counts.overdue > 0 ? "border-red-300/70 bg-red-50/70 text-red-700 dark:border-red-400/50 dark:bg-[rgba(248,113,113,0.12)] dark:text-red-200" : "border-emerald-200/70 bg-emerald-50/80 text-emerald-700 dark:border-emerald-300/40 dark:bg-[rgba(52,211,153,0.12)] dark:text-emerald-200" }`} > - - Overdue Tasks - - - {overdueTasks.isSuccess ? overdueTasks.data.count : "—"} +
+ Overdue Tasks +
+ + {tasks.isSuccess ? counts.overdue : "—"}
diff --git a/packages/app/src/pages/(Home)/HomeIntroduction.tsx b/packages/app/src/pages/(Home)/HomeIntroduction.tsx index 829ae28..b616b39 100644 --- a/packages/app/src/pages/(Home)/HomeIntroduction.tsx +++ b/packages/app/src/pages/(Home)/HomeIntroduction.tsx @@ -34,17 +34,13 @@ export default function HomeIntroduction({ skeleton, user, today }: Introduction -
-
- {getNameByDate(activeDay.getDay() as DaysAsNumbers)} -
- - {getNameByMonth(activeDay.getMonth() as MonthsAsNumbers)} {getDateDD(activeDay)} - -
-

- Sequenced keeps the day light and focused—just the essentials you need to move forward. -

+ + {getNameByDate(activeDay.getDay() as DaysAsNumbers)}, + {getNameByMonth(activeDay.getMonth() as MonthsAsNumbers)} {getDateDD(activeDay)} + ) diff --git a/packages/app/src/pages/(Home)/HomeUpcoming.tsx b/packages/app/src/pages/(Home)/HomeUpcoming.tsx index a52f507..9f20c85 100644 --- a/packages/app/src/pages/(Home)/HomeUpcoming.tsx +++ b/packages/app/src/pages/(Home)/HomeUpcoming.tsx @@ -1,6 +1,6 @@ import { TaskItem } from "@/components/task/TaskItem" -import { useTasksIncomplete } from "@/hooks/tasks"; -import { useNavigate } from "react-router-dom"; +import { Task, useTasksIncomplete } from "@/hooks/tasks"; +import { useNavigate } from "react-router"; import { useApp } from "@/hooks/app"; interface UpcomingParams { @@ -12,7 +12,7 @@ export default function HomeUpcoming({ skeleton }: UpcomingParams) { const navigate = useNavigate(); const [appData, setAppData] = useApp(); - const openTaskInTasks = (task) => { + const openTaskInTasks = (task: Task) => { setAppData({ ...appData, activeTask: task, @@ -34,7 +34,7 @@ export default function HomeUpcoming({ skeleton }: UpcomingParams) {
  • - +
diff --git a/packages/app/src/pages/(Home)/NameProvider.tsx b/packages/app/src/pages/(Home)/NameProvider.tsx index 3ac2e72..1df0628 100644 --- a/packages/app/src/pages/(Home)/NameProvider.tsx +++ b/packages/app/src/pages/(Home)/NameProvider.tsx @@ -1,14 +1,15 @@ import { updateName } from "@/hooks/user"; -import { useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router"; export default function NameProvider() { const navigate = useNavigate(); - const updateInformation = async (e) => { + const updateInformation = async (e: React.FormEvent) => { e.preventDefault(); - const first = e.target[0].value; - const last = e.target[1].value; + const form = e.currentTarget; + const first = (form.elements[0] as HTMLInputElement).value; + const last = (form.elements[1] as HTMLInputElement).value; await updateName(first, last); navigate(0); diff --git a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuEdit.tsx b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuEdit.tsx index ccb12ba..a55c080 100644 --- a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuEdit.tsx +++ b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuEdit.tsx @@ -1,6 +1,15 @@ +import { Task } from "@/hooks/tasks"; import { TaskInfoMenuDelete } from "./Shared/TaskInfoMenuDelete"; -export default function MenuEdit({ type, appData, tempData, isDeleting, setIsDeleting, setIsOpen }) { +interface MenuEditProps { + type: string; + tempData: Task; + isDeleting: boolean; + setIsDeleting: (state: boolean) => void; + setIsOpen: (state: boolean) => void; +} + +export default function MenuEdit({ type, tempData, isDeleting, setIsDeleting, setIsOpen }: MenuEditProps) { const closeMenu = (e?: React.MouseEvent) => { if (e) e.stopPropagation(); diff --git a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuFields.tsx b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuFields.tsx index b3ed357..564313d 100644 --- a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuFields.tsx +++ b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuFields.tsx @@ -9,9 +9,7 @@ interface MenuFieldsProps { isDeleting: boolean; tempData: any; setTempData: any; - setIsOpen: any; changeAppDate: any; - changeTempAppDate: any; appData: any; setAppData: any; quickTasksInput: string; @@ -26,9 +24,7 @@ export default function MenuFields({ isDeleting, tempData, setTempData, - setIsOpen, changeAppDate, - changeTempAppDate, appData, setAppData, quickTasksInput, diff --git a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuHeader.tsx b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuHeader.tsx index 1ad5c33..4399a6b 100644 --- a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuHeader.tsx +++ b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuHeader.tsx @@ -1,6 +1,6 @@ import { Description, DialogTitle } from "@headlessui/react"; -export default function MenuHeader({ isDeleting, type }) { +export default function MenuHeader({ isDeleting, type }: { isDeleting: boolean, type: string }) { return (
diff --git a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoMenuDelete.tsx b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoMenuDelete.tsx index cb6229f..cfd6da6 100644 --- a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoMenuDelete.tsx +++ b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoMenuDelete.tsx @@ -1,19 +1,20 @@ -import { useDeleteTask } from "@/hooks/tasks"; +import { Task, useDeleteTask } from "@/hooks/tasks"; -export function TaskInfoMenuDelete({ - task, - closeMenu, - isDeleting, - setIsDeleting, - parent -}) { +interface TaskInfoMenuDeleteProps { + task: Task; + closeMenu?: () => void; + isDeleting: boolean; + setIsDeleting: (state: boolean) => void; +} + +export function TaskInfoMenuDelete({ task, closeMenu, isDeleting, setIsDeleting }: TaskInfoMenuDeleteProps) { const { mutate: deleteTask } = useDeleteTask(); const setDeleteTask = async () => { // Close the menu immediately to avoid lingering after deletion. closeMenu?.(); setIsDeleting(false); - await deleteTask(task); + deleteTask(task); }; return ( diff --git a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoUser/TaskInfoMenuUser.tsx b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoUser/TaskInfoMenuUser.tsx index 5932edb..1103533 100644 --- a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoUser/TaskInfoMenuUser.tsx +++ b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoUser/TaskInfoMenuUser.tsx @@ -1,12 +1,12 @@ import { MinusIcon, PlusIcon } from "@heroicons/react/20/solid"; import { useState } from "react"; import TaskInfoMenuUserInvite from "./TaskInfoMenuUserInvite"; -import { useRemoveUser, useTaskUsers } from "@/hooks/tasks"; +import { Task, useRemoveUser, useTaskUsers } from "@/hooks/tasks"; import { queryClient } from "@/index"; -import { useUser } from "@/hooks/user"; +import { User, useUser } from "@/hooks/user"; import { Logger } from "@/utils/logger"; -export default function TaskInfoMenuUser({ data }) { +export default function TaskInfoMenuUser({ data }: { data: Task }) { const host = useUser(); const [addingUser, setAddingUser] = useState(false); @@ -14,7 +14,7 @@ export default function TaskInfoMenuUser({ data }) { const { mutate: removeUser } = useRemoveUser(); - const users = useTaskUsers(data.id); + const users = useTaskUsers(data.id!); if (users.isLoading) return "Loading..."; @@ -24,7 +24,7 @@ export default function TaskInfoMenuUser({ data }) { if (users.isSuccess) { const raw = users.data; - const userList = Array.isArray(raw) ? raw : (raw?.users ?? []); + const userList: User[] = Array.isArray(raw) ? raw : (raw?.users ?? []); return (
@@ -59,7 +59,7 @@ export default function TaskInfoMenuUser({ data }) { return; } - removeUser({ taskId: data.id, userEmail: user.email }); + removeUser({ taskId: data.id!, userEmail: user.email }); setStatus({ status: "Success", message: "User removed" }); setTimeout(() => { queryClient.invalidateQueries({ queryKey: ["tasks", data.id, "users"] }); diff --git a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoUser/TaskInfoMenuUserInvite.tsx b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoUser/TaskInfoMenuUserInvite.tsx index dec7581..a03c030 100644 --- a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoUser/TaskInfoMenuUserInvite.tsx +++ b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoUser/TaskInfoMenuUserInvite.tsx @@ -1,16 +1,16 @@ +import { Task } from "@/hooks/tasks"; import { useUser } from "@/hooks/user"; import { queryClient } from "@/index"; import { fetchData } from "@/utils/data"; -export default function TaskInfoMenuUserInvite({ task, setStatus }) { - +export default function TaskInfoMenuUserInvite({ task, setStatus }: { task: Task, setStatus: (_: { status: string, message: string }) => void }) { const user = useUser(); const inviteUser = async () => { - const invitee = document.getElementById("user-invite-email"); + const invitee = document.getElementById("user-invite-email") as HTMLInputElement; if (!invitee || !invitee.value) return; - if (invitee.value == user.data.email) { + if (invitee.value == user.data?.email) { setStatus({ status: "Error", message: "You cannot invite yourself." }); return; } diff --git a/packages/app/src/pages/(Layout)/DataContainer.tsx b/packages/app/src/pages/(Layout)/DataContainer.tsx index 2e96e8c..67760ba 100644 --- a/packages/app/src/pages/(Layout)/DataContainer.tsx +++ b/packages/app/src/pages/(Layout)/DataContainer.tsx @@ -1,4 +1,4 @@ -import { Outlet, useLocation } from "react-router-dom"; +import { Outlet, useLocation } from "react-router"; export default function DataContainer() { const location = useLocation(); @@ -16,6 +16,7 @@ export default function DataContainer() {
diff --git a/packages/app/src/pages/(Layout)/Icons/CalendarViewIcon.tsx b/packages/app/src/pages/(Layout)/Icons/CalendarViewIcon.tsx index 98804c3..33b643a 100644 --- a/packages/app/src/pages/(Layout)/Icons/CalendarViewIcon.tsx +++ b/packages/app/src/pages/(Layout)/Icons/CalendarViewIcon.tsx @@ -1,5 +1,18 @@ -import calendarView from "@/assets/calendar-view.svg"; - export default function CalendarViewIcon({ className = "" }: { className?: string }) { - return Calendar view placeholder; + return ( + + ); } diff --git a/packages/app/src/pages/(Layout)/Nav/NavBar.tsx b/packages/app/src/pages/(Layout)/Nav/NavBar.tsx index b7c1a7b..63ab9eb 100644 --- a/packages/app/src/pages/(Layout)/Nav/NavBar.tsx +++ b/packages/app/src/pages/(Layout)/Nav/NavBar.tsx @@ -5,7 +5,6 @@ import { useState } from "react"; import TaskInfoMenu from "../TaskInfoMenu"; import TasksIcon from "../Icons/TasksIcon"; import NavItem from "./NavItem"; -import ListsIcon from "../Icons/ListsIcon"; import { useAuth } from "@/hooks/auth"; import CalendarViewIcon from "../Icons/CalendarViewIcon"; @@ -15,7 +14,7 @@ export function NavBar() { const [isAdding, setIsAdding] = useState(false); const renderMobileBar = (isInteractive: boolean) => ( -
+
- - + + @@ -78,8 +77,8 @@ export function NavBar() { - - + + diff --git a/packages/app/src/pages/(Layout)/Nav/NavItem.tsx b/packages/app/src/pages/(Layout)/Nav/NavItem.tsx index 053b163..eef731f 100644 --- a/packages/app/src/pages/(Layout)/Nav/NavItem.tsx +++ b/packages/app/src/pages/(Layout)/Nav/NavItem.tsx @@ -1,5 +1,5 @@ import { ReactNode } from "react"; -import { Link, useLocation } from "react-router-dom"; +import { Link, useLocation } from "react-router"; interface NavItemProps { to: string; diff --git a/packages/app/src/pages/(Layout)/TaskInfoMenu.tsx b/packages/app/src/pages/(Layout)/TaskInfoMenu.tsx index 61cdd67..730576b 100644 --- a/packages/app/src/pages/(Layout)/TaskInfoMenu.tsx +++ b/packages/app/src/pages/(Layout)/TaskInfoMenu.tsx @@ -5,7 +5,6 @@ import { createInitialTaskData, useAddTask, useAddTasksBulk, - useTaskById, useUpdateTask, } from "@/hooks/tasks"; @@ -158,17 +157,10 @@ export default function TaskInfoMenu({ }); }; - const changeTempAppDate = (date: Date) => { - setAppData({ - ...appData, - tempActiveDate: date, - }); - }; - const createNotification = async (task: Task) => { if (!task || task.reminder == "") return; - const setDate: Date = task.date || new Date(); + const setDate: Date = new Date(task.date); const second = 1000; const minute = second * 60; @@ -230,8 +222,6 @@ export default function TaskInfoMenu({ setIsOpen(false); }; - const oldTask = useTaskById(tempData.id); - const showToast = (text: string, variant: "create" | "update" | "bulk") => { setToast({ text, variant }); setTimeout(() => setToast(null), 2400); @@ -324,7 +314,7 @@ export default function TaskInfoMenu({ setAppData({ ...appData, activeDate: appData.storedDate, - storedDate: null + storedDate: undefined }); } @@ -333,8 +323,8 @@ export default function TaskInfoMenu({ title: tempData.title.trim(), }; - addTask(cleanedTask); - createNotification(cleanedTask); + addTask(cleanedTask as Task); + createNotification(cleanedTask as Task); showToast("Task created", "create"); resetForm(); @@ -352,18 +342,13 @@ export default function TaskInfoMenu({ onClose={() => resetForm()} initialFocus={ref} ref={ref} - className="relative z-50" + className="relative z-50 w-screen h-screen bg-black/25 dark:bg-black/60 backdrop-blur-sm" > -
- -
+
+ +
diff --git a/packages/app/src/pages/(Settings)/ControlledUser.tsx b/packages/app/src/pages/(Settings)/ControlledUser.tsx index 48862f4..7d20cce 100644 --- a/packages/app/src/pages/(Settings)/ControlledUser.tsx +++ b/packages/app/src/pages/(Settings)/ControlledUser.tsx @@ -1,11 +1,11 @@ import { fetchData } from "@/utils/data" -import { useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router"; export default function ControllerUser() { const navigate = useNavigate(); const loginAsUser = async () => { - const id = document.getElementById("developer_uid")?.value; + const id = (document.getElementById("developer_uid") as HTMLInputElement).value; const res = await (await fetchData("/auth/loginAsUser", { method: "POST", diff --git a/packages/app/src/pages/(Settings)/DailyNotifications.tsx b/packages/app/src/pages/(Settings)/DailyNotifications.tsx index 3fa827f..aedbd1c 100644 --- a/packages/app/src/pages/(Settings)/DailyNotifications.tsx +++ b/packages/app/src/pages/(Settings)/DailyNotifications.tsx @@ -10,7 +10,7 @@ export default function DailyNotifications({ tempSettings, UpdateSettings }: Dai return ( UpdateSettings({ sendDailyReminders: val }) } diff --git a/packages/app/src/pages/(Settings)/DeveloperSettings.tsx b/packages/app/src/pages/(Settings)/DeveloperSettings.tsx index 368b98f..e89a28d 100644 --- a/packages/app/src/pages/(Settings)/DeveloperSettings.tsx +++ b/packages/app/src/pages/(Settings)/DeveloperSettings.tsx @@ -1,6 +1,6 @@ import { useUser } from "@/hooks/user"; -export default function DeveloperSettings({ children }) { +export default function DeveloperSettings({ children }: React.PropsWithChildren) { const user = useUser(); if (user.isLoading) diff --git a/packages/app/src/pages/(Settings)/UserLogin.tsx b/packages/app/src/pages/(Settings)/UserLogin.tsx index 3c43150..fd108d7 100644 --- a/packages/app/src/pages/(Settings)/UserLogin.tsx +++ b/packages/app/src/pages/(Settings)/UserLogin.tsx @@ -1,6 +1,6 @@ import { useUser } from "@/hooks/user"; import { signout } from "@/hooks/auth"; -import { useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router"; import { fetchData } from "@/utils/data"; export default function UserLogin() { diff --git a/packages/app/src/pages/Auth/AuthProvider.tsx b/packages/app/src/pages/Auth/AuthProvider.tsx index abd6181..afc8c20 100644 --- a/packages/app/src/pages/Auth/AuthProvider.tsx +++ b/packages/app/src/pages/Auth/AuthProvider.tsx @@ -1,7 +1,7 @@ import { useAuth } from "@/hooks/auth"; -import { Navigate } from "react-router-dom"; +import { Navigate } from "react-router"; -export default function AuthProvider({ children }) { +export default function AuthProvider({ children }: React.PropsWithChildren) { const auth = useAuth(); if (auth.isError) diff --git a/packages/app/src/pages/Auth/ForgotPassword.tsx b/packages/app/src/pages/Auth/ForgotPassword.tsx new file mode 100644 index 0000000..1c512b1 --- /dev/null +++ b/packages/app/src/pages/Auth/ForgotPassword.tsx @@ -0,0 +1,162 @@ +import { useState } from "react"; +import { useNavigate, useSearchParams } from "react-router"; +import ArrowBack from "../(Login)/ArrowBack"; +import { useCompletePasswordReset, useRequestPasswordReset } from "@/hooks/auth"; + +export default function ForgotPassword() { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const token = searchParams.get("token") ?? ""; + + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirm, setConfirm] = useState(""); + const [status, setStatus] = useState(""); + const [error, setError] = useState(""); + + const requestReset = useRequestPasswordReset(); + const completeReset = useCompletePasswordReset(); + + const handleRequest = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + setStatus(""); + + try { + await requestReset.mutateAsync({ email }); + setStatus("If that email is registered, a reset link is on the way."); + } catch (err: any) { + setError(err?.message || "Unable to send reset link right now."); + } + }; + + const handleReset = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + setStatus(""); + + if (!token) { + setError("Reset link is missing or invalid."); + return; + } + + if (password !== confirm) { + setError("Passwords do not match."); + return; + } + + try { + await completeReset.mutateAsync({ token, password }); + setStatus("Password updated. You can now sign in."); + setPassword(""); + setConfirm(""); + } catch (err: any) { + setError(err?.message || "Unable to reset password."); + } + }; + + const isResetMode = Boolean(token); + + return ( +
+
+
+
+ + + {isResetMode ? "Reset password" : "Forgot password"} + +
+
+ + {!isResetMode && ( +
+

+ Enter your account email and we'll send you a reset link. +

+ + {error && {error}} + {status && {status}} + +
+ )} + + {isResetMode && ( +
+

+ Create a new password for your Sequenced account. +

+ + + {error && {error}} + {status && {status}} + +
+ +
+
+ )} +
+
+ ); +} diff --git a/packages/app/src/pages/Auth/LoginHome.tsx b/packages/app/src/pages/Auth/LoginHome.tsx index f8333b8..89210b8 100644 --- a/packages/app/src/pages/Auth/LoginHome.tsx +++ b/packages/app/src/pages/Auth/LoginHome.tsx @@ -1,7 +1,7 @@ import icon from "@/assets/icon.png"; import TaskFeature from "../(Login)/TaskFeature"; -import { useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router"; export default function LoginHome() { const navigate = useNavigate(); diff --git a/packages/app/src/pages/Auth/LoginUser.tsx b/packages/app/src/pages/Auth/LoginUser.tsx index 7a3dd6d..2d06af0 100644 --- a/packages/app/src/pages/Auth/LoginUser.tsx +++ b/packages/app/src/pages/Auth/LoginUser.tsx @@ -1,4 +1,4 @@ -import { useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router"; import ArrowBack from "../(Login)/ArrowBack"; import { reloadAuth, useLogin } from "@/hooks/auth"; import { useState } from "react"; @@ -13,11 +13,12 @@ export default function LoginUser() { const [status, setStatus] = useState(""); - const loginUser = async (e) => { + const loginUser = async (e: React.FormEvent) => { e.preventDefault(); - const email = e.target[0].value; - const password = e.target[1].value; + const form = e.currentTarget; + const email = (form.elements[0] as HTMLInputElement).value; + const password = (form.elements[1] as HTMLInputElement).value; const resp = await login({ email, password }); @@ -66,7 +67,13 @@ export default function LoginUser() { {status.length > 0 && {status}}
- Forgot Password? +
diff --git a/packages/app/src/pages/Auth/RegisterUser.tsx b/packages/app/src/pages/Auth/RegisterUser.tsx index bff1d00..7626311 100644 --- a/packages/app/src/pages/Auth/RegisterUser.tsx +++ b/packages/app/src/pages/Auth/RegisterUser.tsx @@ -1,4 +1,4 @@ -import { useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router"; import ArrowBack from "../(Login)/ArrowBack"; import { reloadAuth, useRegister } from "@/hooks/auth"; import { useState } from "react"; @@ -13,13 +13,14 @@ export default function RegisterUser() { const [status, setStatus] = useState(""); - const registerUser = async (e) => { + const registerUser = async (e: React.FormEvent) => { e.preventDefault(); - const first = e.target[0].value; - const last = e.target[1].value; - const email = e.target[2].value; - const password = e.target[3].value; + const form = e.currentTarget; + const first = (form.elements[0] as HTMLInputElement).value; + const last = (form.elements[1] as HTMLInputElement).value; + const email = (form.elements[2] as HTMLInputElement).value; + const password = (form.elements[3] as HTMLInputElement).value; const response = await register({ first, last, email, password }); diff --git a/packages/app/src/pages/Calendar.tsx b/packages/app/src/pages/Calendar.tsx new file mode 100644 index 0000000..bae6cca --- /dev/null +++ b/packages/app/src/pages/Calendar.tsx @@ -0,0 +1,471 @@ +import { useMemo, useState } from "react"; +import { useNavigate, useSearchParams } from "react-router"; +import { useTasks, Task } from "@/hooks/tasks"; +import { occursOnDate, isTaskDone } from "@/utils/data"; +import TaskInfoMenu from "@/pages/(Layout)/TaskInfoMenu"; +import { useApp } from "@/hooks/app"; + +type Scope = "today" | "tomorrow" | "week" | "month" | "overdue" | "all"; +type ViewMode = "week" | "month"; + +const startOfWeek = (reference: Date) => { + const day = reference.getDay(); + const diff = (day === 0 ? -6 : 1) - day; // Monday start + const start = new Date(reference); + start.setHours(0, 0, 0, 0); + start.setDate(reference.getDate() + diff); + return start; +}; + +const normalizeDay = (date: Date) => { + const d = new Date(date); + d.setHours(0, 0, 0, 0); + return d; +}; + +const isPendingOnDate = (task: Task, day: Date) => { + // Leverage existing helper that returns true when still pending. + return isTaskDone(task, day) && occursOnDate(task, day); +}; + +const isOverdue = (task: Task, today: Date) => { + const taskDate = normalizeDay(new Date(task.date)); + return isPendingOnDate(task, taskDate) && taskDate < today; +}; + +const dayKey = (date: Date) => date.toISOString().slice(0, 10); + +const formatLabel = (date: Date, options: Intl.DateTimeFormatOptions) => + date.toLocaleDateString(undefined, options); + +function buildWeekDays(anchor: Date) { + const start = startOfWeek(anchor); + return Array.from({ length: 7 }).map((_, idx) => { + const day = new Date(start); + day.setDate(start.getDate() + idx); + return day; + }); +} + +function buildMonthDays(anchor: Date): Array { + const firstOfMonth = new Date(anchor.getFullYear(), anchor.getMonth(), 1); + const lastOfMonth = new Date(anchor.getFullYear(), anchor.getMonth() + 1, 0); + + // Start on Monday of the first week containing the 1st. + const gridStart = startOfWeek(firstOfMonth); + + const days: Array = []; + for (let d = new Date(gridStart); d <= lastOfMonth; d.setDate(d.getDate() + 1)) { + days.push(new Date(d)); + } + + // Pad trailing cells to keep 7 per row, but do not show next-month dates. + const remainder = days.length % 7; + if (remainder !== 0) { + for (let i = remainder; i < 7; i++) days.push(null); + } + + return days; +} + +const scopeToDates = (scope: Scope, today: Date) => { + const start = normalizeDay(today); + const tomorrow = new Date(start); + tomorrow.setDate(start.getDate() + 1); + const endOfWeek = new Date(startOfWeek(today)); + endOfWeek.setDate(endOfWeek.getDate() + 6); + + switch (scope) { + case "today": + return { start, end: start }; + case "tomorrow": + return { start: tomorrow, end: tomorrow }; + case "week": + return { start: startOfWeek(today), end: endOfWeek }; + case "month": + return { + start: new Date(today.getFullYear(), today.getMonth(), 1), + end: new Date(today.getFullYear(), today.getMonth() + 1, 0), + }; + case "overdue": + return { start: new Date(0), end: new Date(today.getTime() - 1) }; + default: + return { start: new Date(0), end: new Date(8640000000000000) }; // all dates + } +}; + +function groupTasksByDay(tasks: Task[], start: Date, end: Date) { + const grouped: Record = {}; + + for (const task of tasks) { + // Include every day in range where the task is pending, regardless of start date. + for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { + const day = new Date(d); + if (isPendingOnDate(task, day)) { + const key = dayKey(day); + if (!grouped[key]) grouped[key] = []; + grouped[key].push(task); + } + } + } + + return grouped; +} + +export default function CalendarPage() { + const navigate = useNavigate(); + const [params] = useSearchParams(); + const tasks = useTasks(); + const [appState, setAppState] = useApp(); + const [isTaskMenuOpen, setIsTaskMenuOpen] = useState(false); + + const today = normalizeDay(new Date()); + const initialScope = (params.get("scope") as Scope) || "week"; + const initialView = (params.get("view") as ViewMode) || "week"; + const initialWeekParam = params.get("week"); + const initialMonthParam = params.get("month"); + + const resolveWeekStart = (scope: Scope) => { + if (scope === "tomorrow") { + const tomorrow = new Date(today); + tomorrow.setDate(today.getDate() + 1); + return startOfWeek(tomorrow); + } + return startOfWeek(today); + }; + + const [scope, setScope] = useState(initialScope); + const [view, setView] = useState(initialView); + const [weekStart, setWeekStart] = useState( + initialWeekParam ? normalizeDay(new Date(initialWeekParam)) : resolveWeekStart(initialScope) + ); + const [monthAnchor, setMonthAnchor] = useState(() => { + const base = initialMonthParam ? new Date(initialMonthParam) : today; + return new Date(base.getFullYear(), base.getMonth(), 1); + }); + + const { start, end } = useMemo(() => { + if (view === "week") { + const s = normalizeDay(weekStart); + const e = new Date(s); + e.setDate(s.getDate() + 6); + return { start: s, end: e }; + } + + if (view === "month") { + const s = new Date(monthAnchor.getFullYear(), monthAnchor.getMonth(), 1); + const e = new Date(monthAnchor.getFullYear(), monthAnchor.getMonth() + 1, 0); + return { start: s, end: e }; + } + + return scopeToDates(scope, today); + }, [view, weekStart, monthAnchor, scope, today]); + + const grouped = useMemo(() => groupTasksByDay(tasks.data || [], start, end), [tasks.data, start, end]); + + const weekDays = useMemo(() => buildWeekDays(weekStart), [weekStart]); + const monthDays = useMemo(() => buildMonthDays(monthAnchor), [monthAnchor]); + const monthWeeks = useMemo(() => { + const weeks: Array> = []; + for (let i = 0; i < monthDays.length; i += 7) { + weeks.push(monthDays.slice(i, i + 7)); + } + return weeks; + }, [monthDays]); + + const overdueList = useMemo(() => { + if (!tasks.data) return []; + return tasks.data.filter((task) => isOverdue(task, today)); + }, [tasks.data, today]); + + const handleViewChange = (next: ViewMode) => { + setView(next); + if (next === "week") { + navigate(`/calendar?scope=${scope}&view=${next}&week=${dayKey(weekStart)}`, { replace: true }); + } else { + const monthKey = new Date(monthAnchor.getFullYear(), monthAnchor.getMonth(), 1).toISOString().slice(0, 10); + navigate(`/calendar?scope=${scope}&view=${next}&month=${monthKey}`, { replace: true }); + } + }; + + const changeWeek = (delta: number) => { + const next = new Date(weekStart); + next.setDate(weekStart.getDate() + delta * 7); + setWeekStart(next); + setScope("week"); + navigate(`/calendar?scope=week&view=week&week=${dayKey(next)}`, { replace: true }); + }; + + const goToWeek = (startDate: Date) => { + const startNormalized = startOfWeek(startDate); + setWeekStart(startNormalized); + setScope("week"); + setView("week"); + navigate(`/calendar?scope=week&view=week&week=${dayKey(startNormalized)}`, { replace: true }); + }; + + const changeMonth = (delta: number) => { + const next = new Date(monthAnchor); + next.setMonth(monthAnchor.getMonth() + delta); + next.setDate(1); + setMonthAnchor(next); + setView("month"); + const monthKey = next.toISOString().slice(0, 10); + navigate(`/calendar?scope=${scope}&view=month&month=${monthKey}`, { replace: true }); + }; + + const openTask = (task: Task, activeDay?: Date) => { + setAppState({ + ...appState, + activeTask: task, + activeDate: activeDay ?? new Date(task.date), + }); + setIsTaskMenuOpen(true); + }; + + const renderTask = (task: Task, day?: Date) => ( + + ); + + const renderWeek = () => ( +
+ {weekDays.map((day) => { + const key = dayKey(day); + const dayTasks = grouped[key] || []; + const isToday = dayKey(day) === dayKey(today); + return ( +
+
+
+
+ {formatLabel(day, { weekday: "short" })} +
+
+ + {formatLabel(day, { month: "long", day: "numeric" })} + + {dayTasks.length} pending +
+
+
+
+ {dayTasks.length === 0 && ( +
+ Nothing due. +
+ )} + {dayTasks.map((task) => renderTask(task, day))} +
+
+ ); + })} +
+ ); + + const renderMonth = () => ( +
+
+ + + {formatLabel(monthAnchor, { month: "long", year: "numeric" })} + + +
+
+ {monthWeeks.map((week, idx) => ( +
week[0] && goToWeek(week[0])} + onKeyDown={(e) => { + if ((e.key === "Enter" || e.key === " ") && week[0]) { + e.preventDefault(); + goToWeek(week[0]); + } + }} + className="grid grid-cols-7 gap-2 sm:gap-3 rounded-2xl border border-slate-200/70 bg-white/70 p-1 shadow-sm transition hover:border-accent-blue/40 hover:ring-1 hover:ring-accent-blue/20 dark:border-slate-700/60 dark:bg-slate-900/70" + > + {week.map((day) => { + if (!day) { + return
; + } + const key = dayKey(day); + const dayTasks = grouped[key] || []; + const isToday = dayKey(day) === dayKey(today); + const isCurrentMonth = day.getMonth() === monthAnchor.getMonth(); + return ( +
+ + {day.getDate()} + +
+ + {dayTasks.length > 0 ? `${dayTasks.length} due` : "—"} + +
+ {dayTasks.slice(0, 3).map((task) => ( + + ))} + {dayTasks.length > 3 && ( + +{dayTasks.length - 3} more + )} + {dayTasks.length === 0 && } +
+
+
+ ); + })} +
+ ))} +
+
+ ); + + return ( +
+
+
+
+

Calendar

+

See what is coming up this week or month.

+
+
+ {(["week", "month"] as ViewMode[]).map((mode) => ( + + ))} +
+
+ + {view === "week" && ( +
+ + + Week of {formatLabel(weekStart, { month: "long", day: "numeric" })} + + +
+ )} + + {scope === "overdue" && ( +
+
+
+ Overdue + Pending tasks before today +
+ {overdueList.length} +
+
+ {overdueList.length === 0 && ( +
+ All caught up! +
+ )} + {overdueList.map((task) => renderTask(task))} +
+
+ )} + +
+ {tasks.isLoading && ( +
+ Loading calendar... +
+ )} + {tasks.isSuccess && ( + <> + {view === "week" ? renderWeek() : renderMonth()} + + + )} +
+
+
+ ); +} diff --git a/packages/app/src/pages/Home.tsx b/packages/app/src/pages/Home.tsx index 66f4206..ad0d3f1 100644 --- a/packages/app/src/pages/Home.tsx +++ b/packages/app/src/pages/Home.tsx @@ -1,5 +1,5 @@ import { useUser } from "@/hooks/user"; -import { Navigate } from "react-router-dom"; +import { Navigate } from "react-router"; import { useAuth } from "@/hooks/auth"; import AuthProvider from "./Auth/AuthProvider"; @@ -20,18 +20,18 @@ const Home = () => { if (auth.isLoading) return ( -
+
- - - + + +
) return ( -
+
diff --git a/packages/app/src/pages/Layout.tsx b/packages/app/src/pages/Layout.tsx index 5a28558..1713869 100644 --- a/packages/app/src/pages/Layout.tsx +++ b/packages/app/src/pages/Layout.tsx @@ -4,15 +4,9 @@ import { NavBar } from "./(Layout)/Nav/NavBar"; const Layout = () => { return (
-
-
-
- -
-
-
-
+ +
); }; diff --git a/packages/app/src/pages/Settings.tsx b/packages/app/src/pages/Settings.tsx index 7a15764..9028256 100644 --- a/packages/app/src/pages/Settings.tsx +++ b/packages/app/src/pages/Settings.tsx @@ -132,7 +132,7 @@ export default function SettingsPage() { const minute = parseInt(timeParts[1]); await cancelNotification(await FindDailyTask()); - const newReminder = await setDailyReminders(hour, minute); + await setDailyReminders(hour, minute); Logger.log(`Set Daily Reminders!`); }; @@ -158,7 +158,7 @@ export default function SettingsPage() { const hour = parseInt(timeParts[0]); const minute = parseInt(timeParts[1]); - const newReminder = await setDailyReminders(hour, minute); + await setDailyReminders(hour, minute); Logger.log(`Set Daily Reminders!`); } @@ -514,15 +514,62 @@ export default function SettingsPage() {

Feedback

-

Spot an issue or have an idea? Drop us a note.

- +

Share a quick rating or leave us a note.

+
+
+ +
+ {[1, 2, 3, 4, 5].map((value) => { + const isActive = reviewRating >= value; + return ( + + ); + })} +
+ Tap to set 1-5 stars. +
+