From 44c938b5530a80e0e4aeaff87278ad828ff2b5f5 Mon Sep 17 00:00:00 2001 From: Paul Hovley Date: Wed, 9 Oct 2024 19:52:36 -0600 Subject: [PATCH] Issue 237/email report (#244) * Added weekly report * refactor for arrow functions and preferring named imports --- .eslintrc.js | 11 + .gitignore | 6 + bin/migrations/20240620003646_add_pulses.js | 1 + bin/migrations/20241003221416_add_user.js | 36 +++ bin/migrations/20241004000814_create_user.js | 26 ++ docs/Contributing.md | 5 +- package-lock.json | 262 +++++++++++++++++- package.json | 4 +- packages/app/src/App.tsx | 21 +- packages/app/src/api/index.ts | 25 ++ packages/app/src/api/keys.ts | 20 ++ packages/app/src/api/pulse.api.ts | 26 ++ packages/app/src/api/repos/pulse.repo.ts | 28 ++ .../src/api/repos/queries/deepWork.query.ts | 36 +++ packages/app/src/api/repos/user.repo.ts | 39 +++ .../app/src/api/services/pulse.service.ts | 52 ++++ .../src/{ => api}/services/query.service.ts | 8 +- packages/app/src/api/user.api.ts | 65 +++++ .../sql.util.ts => api/utils/db.util.ts} | 35 ++- packages/app/src/assets/icons/bar_chart.svg | 3 + packages/app/src/assets/icons/block.svg | 3 + packages/app/src/assets/icons/boss.png | Bin 0 -> 108036 bytes .../app/src/assets/icons/notification.svg | 4 + .../app/src/components/ContributorsPage.tsx | 6 +- .../components/Extensions/ExtensionDetail.tsx | 15 +- .../Extensions/ExtensionsDashboard.tsx | 11 +- .../components/Extensions/ExtensionsPage.tsx | 6 +- packages/app/src/components/Home/DeepWork.tsx | 13 +- .../Home/Extensions/ExtensionsWidget.tsx | 17 +- .../Home/{Header.tsx => HomeHeader.tsx} | 4 +- packages/app/src/components/Home/HomePage.tsx | 6 +- .../src/components/Home/Source/AddSources.tsx | 8 +- .../components/Home/Source/Sources.empty.tsx | 6 +- .../components/Home/Source/Sources.error.tsx | 2 +- .../Home/Source/Sources.loading.tsx | 4 +- .../src/components/Home/Source/Sources.tsx | 21 +- .../components/Home/Time/CategoryChart.tsx | 4 +- .../app/src/components/Home/Time/Time.tsx | 48 +++- packages/app/src/components/ImportPage.tsx | 8 +- packages/app/src/components/InstallPage.tsx | 4 +- packages/app/src/components/UpdatePage.tsx | 2 +- .../components/common/CodeClimbersButton.tsx | 2 +- .../common/CodeClimbersIconButton.tsx | 2 +- .../common/CodeClimbersLoadingButton.tsx | 2 +- .../common/CodeSnippit/CodeSnippit.tsx | 2 +- .../components/common/Icons/BarChartIcon.tsx | 17 ++ .../src/components/common/Icons/BlockIcon.tsx | 15 + .../src/components/common/Icons/BossImage.tsx | 11 + .../common/Icons/NotificationIcon.tsx | 19 ++ .../common/LocalApiKeyErrorBanner.tsx | 2 +- .../app/src/components/common/PlainHeader.tsx | 4 +- .../components/common/WeeklyReportDialog.tsx | 216 +++++++++++++++ packages/app/src/config/theme.ts | 2 +- .../src/extensions/SqlSandbox/SqlSandbox.tsx | 8 +- .../extensions/SqlSandbox/SqlSandboxPage.tsx | 20 +- .../app/src/extensions/SqlSandbox/index.tsx | 4 +- .../src/extensions/SqlSandbox/sandbox.api.ts | 4 +- .../SqlSandbox/sqlSandbox.service.ts | 2 +- packages/app/src/hooks/useBrowserStorage.ts | 4 +- packages/app/src/hooks/useUpdateHook.ts | 6 +- packages/app/src/layouts/DashboardLayout.tsx | 4 +- packages/app/src/layouts/ExtensionsLayout.tsx | 8 +- packages/app/src/layouts/ImportLayout.tsx | 4 +- packages/app/src/layouts/InstallLayout.tsx | 4 +- .../providers/localStorageAuthProvider.tsx | 6 +- packages/app/src/repos/pulse.repo.ts | 40 ++- .../app/src/repos/queries/deepWork.query.ts | 36 +++ packages/app/src/repos/user.repo.ts | 39 +++ packages/app/src/routes/AppRoutes.tsx | 12 +- packages/app/src/routes/index.tsx | 4 +- .../app/src/services/contributors.service.ts | 2 +- .../app/src/services/extensions.service.ts | 36 +-- packages/app/src/services/health.service.ts | 6 +- packages/app/src/services/keys.ts | 4 + .../app/src/services/localAuth.service.ts | 6 +- packages/app/src/services/pulse.service.ts | 64 ++--- packages/app/src/services/version.service.ts | 6 +- packages/app/src/utils/auth.util.ts | 6 +- packages/app/src/utils/csv.util.ts | 5 +- packages/app/src/utils/db.util.ts | 14 +- packages/app/src/utils/environment.util.ts | 2 +- packages/app/src/utils/request.ts | 16 +- packages/app/src/utils/time.ts | 2 +- packages/server/commands/start/index.ts | 2 +- packages/server/src/main.ts | 2 +- .../src/v1/activities/activities.service.ts | 29 +- packages/server/src/v1/database/knex.ts | 4 +- .../server/src/v1/database/models/user.d.ts | 21 ++ .../src/v1/database/models/user_setting.d.ts | 18 ++ packages/server/src/v1/database/pulse.repo.ts | 15 +- .../src/v1/startup/darwinStartup.service.ts | 4 +- .../src/v1/startup/linuxStartup.service.ts | 4 +- .../server/src/v1/startup/startup.util.ts | 6 +- .../src/v1/startup/windowsStartup.service.ts | 4 +- .../utils/__tests__/activites.util.test.ts | 4 +- packages/server/utils/activities.util.ts | 31 +-- packages/server/utils/helpers.util.ts | 26 +- packages/server/utils/ini.util.ts | 16 +- packages/server/utils/localAuth.util.ts | 12 +- packages/server/utils/node.util.ts | 5 +- packages/server/utils/sqlReader.util.ts | 8 +- scripts/mock_install.sh | 39 ++- 102 files changed, 1470 insertions(+), 349 deletions(-) create mode 100644 bin/migrations/20241003221416_add_user.js create mode 100644 bin/migrations/20241004000814_create_user.js create mode 100644 packages/app/src/api/index.ts create mode 100644 packages/app/src/api/keys.ts create mode 100644 packages/app/src/api/pulse.api.ts create mode 100644 packages/app/src/api/repos/pulse.repo.ts create mode 100644 packages/app/src/api/repos/queries/deepWork.query.ts create mode 100644 packages/app/src/api/repos/user.repo.ts create mode 100644 packages/app/src/api/services/pulse.service.ts rename packages/app/src/{ => api}/services/query.service.ts (62%) create mode 100644 packages/app/src/api/user.api.ts rename packages/app/src/{utils/sql.util.ts => api/utils/db.util.ts} (50%) create mode 100644 packages/app/src/assets/icons/bar_chart.svg create mode 100644 packages/app/src/assets/icons/block.svg create mode 100644 packages/app/src/assets/icons/boss.png create mode 100644 packages/app/src/assets/icons/notification.svg rename packages/app/src/components/Home/{Header.tsx => HomeHeader.tsx} (97%) create mode 100644 packages/app/src/components/common/Icons/BarChartIcon.tsx create mode 100644 packages/app/src/components/common/Icons/BlockIcon.tsx create mode 100644 packages/app/src/components/common/Icons/BossImage.tsx create mode 100644 packages/app/src/components/common/Icons/NotificationIcon.tsx create mode 100644 packages/app/src/components/common/WeeklyReportDialog.tsx create mode 100644 packages/app/src/repos/queries/deepWork.query.ts create mode 100644 packages/app/src/repos/user.repo.ts create mode 100644 packages/server/src/v1/database/models/user.d.ts create mode 100644 packages/server/src/v1/database/models/user_setting.d.ts diff --git a/.eslintrc.js b/.eslintrc.js index b359056d..2060c929 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -38,5 +38,16 @@ module.exports = { "@typescript-eslint/no-namespace": "off", "@typescript-eslint/no-explicit-any": "error", "codeclimbers/use-code-climbers-button": "error", + "prefer-arrow-callback": "warn", + "func-style": ["warn", "expression", { "allowArrowFunctions": true }], + "import/no-default-export": "error", }, + overrides: [ + { + files: ["packages/server/commands/**/*.ts"], + rules: { + "import/no-default-export": "off" + } + } + ] }; diff --git a/.gitignore b/.gitignore index 85faa1ba..7179e956 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,9 @@ bin/daemon # firebase cache .firebase/hosting.* + +# npm pack and tarball files +*.tgz + +# mock install +codeclimbers_install_* \ No newline at end of file diff --git a/bin/migrations/20240620003646_add_pulses.js b/bin/migrations/20240620003646_add_pulses.js index 7fddc2ca..cb7345e1 100644 --- a/bin/migrations/20240620003646_add_pulses.js +++ b/bin/migrations/20240620003646_add_pulses.js @@ -19,6 +19,7 @@ const SQL = `--sql origin_id varchar(255), created_at timestamp(3), description text + ); ` exports.up = function (knex) { diff --git a/bin/migrations/20241003221416_add_user.js b/bin/migrations/20241003221416_add_user.js new file mode 100644 index 00000000..7d9af122 --- /dev/null +++ b/bin/migrations/20241003221416_add_user.js @@ -0,0 +1,36 @@ +const SQL = `--sql + CREATE TABLE accounts_user ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + email varchar(255) UNIQUE, + first_name varchar(255), + last_name varchar(255), + avatar_url varchar(255), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + +` + +const SQL_SETTINGS = `--sql + CREATE TABLE IF NOT EXISTS accounts_user_settings ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + user_id INTEGER NOT NULL, + weekly_report_type VARCHAR(255) NOT NULL DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES accounts_user (id) ON DELETE CASCADE + ); +` + +exports.up = async function (knex) { + await knex.raw(SQL) + await knex.raw(SQL_SETTINGS) + return +} + +exports.down = function (knex) { + return +// return knex.raw(`--sql +// DROP TABLE accounts_user; +// `) +} diff --git a/bin/migrations/20241004000814_create_user.js b/bin/migrations/20241004000814_create_user.js new file mode 100644 index 00000000..0e087979 --- /dev/null +++ b/bin/migrations/20241004000814_create_user.js @@ -0,0 +1,26 @@ +exports.up = function(knex) { + return knex.transaction(async (trx) => { + // Insert into accounts_user + await trx('accounts_user').insert({}); + const [row] = await trx.raw('SELECT last_insert_rowid() as id'); + const userId = row.id; + // Insert into accounts_user_settings + await trx('accounts_user_settings').insert({ + user_id: userId + }); + }); +}; + +exports.down = function(knex) { + // return knex.transaction(async (trx) => { + // // Remove the last inserted user_settings + // await trx('accounts_user_settings') + // .where('user_id', knex.raw('(SELECT MAX(id) FROM accounts_user)')) + // .del(); + + // // Remove the last inserted user + // await trx('accounts_user') + // .where('id', knex.raw('(SELECT MAX(id) FROM accounts_user)')) + // .del(); + // }); +}; \ No newline at end of file diff --git a/docs/Contributing.md b/docs/Contributing.md index c8c577c4..4d4dd5d5 100644 --- a/docs/Contributing.md +++ b/docs/Contributing.md @@ -65,9 +65,10 @@ A React Single Page Application that uses react-router, material-ui, tanstack to - Any api calls should be included in the `api` directory and make use of tanstack - All components should reside in the `components` directory. - All pages should go in the `components` directory and have their own component. +- Functions and variables should be camelCase. Classes should be PascalCase. +- Use named exports when exporting components, functions, variables, etc (no default exports) - All layout components should go in the `layouts` directory. - `services` generally are react queries that fetch data from the backend. -- Primarily use kysely for building sql queries. When the query is complex, it may be best to use raw sql. - Styling: make use of the `sx` attribute for any material-ui customizations. - Styling: make use of the appropriate material-ui components for layouts like `Grid` or `Stack` when possible, but `Box` is a great fallback @@ -106,6 +107,8 @@ A way for the user to interact with the application easily using oclif you want changes to be reflected in the CLI from the server, you will need to build it with `npm run build:server` and then restart the CLI. +A great way to test the CLI is to install it on a machine and then run `npm run mock:install {version} --run` to install an older version of the CLI and test from there. + ### Conventions - TBD diff --git a/package-lock.json b/package-lock.json index 98b15d3a..35cdb374 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "@sentry/nestjs": "^8.30.0", "@sentry/profiling-node": "^8.30.0", "@tanstack/react-query": "^5.48.0", + "bullmq": "^5.15.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "dayjs": "^1.11.12", @@ -1761,6 +1762,12 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "license": "MIT" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2698,6 +2705,84 @@ "node": ">=8" } }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@mui/base": { "version": "5.0.0-beta.40", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", @@ -6360,6 +6445,34 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/bullmq": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.15.0.tgz", + "integrity": "sha512-h53shVjx8s6wxYGtUfzAfENpSP7N5T0D4PMTvbZncozLjb8yUKhopfpa7PmcpQfq7SSO9dm/OZ9XQuGOCSGNug==", + "license": "MIT", + "dependencies": { + "cron-parser": "^4.6.0", + "ioredis": "^5.4.1", + "msgpackr": "^1.10.1", + "node-abort-controller": "^3.1.1", + "semver": "^7.5.4", + "tslib": "^2.0.0", + "uuid": "^9.0.0" + } + }, + "node_modules/bullmq/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -6743,6 +6856,15 @@ "node": ">=6" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -7109,6 +7231,18 @@ "dev": true, "license": "MIT" }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "license": "MIT", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -7357,6 +7491,15 @@ "license": "MIT", "optional": true }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -9997,6 +10140,30 @@ "node": ">= 0.10" } }, + "node_modules/ioredis": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz", + "integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -12484,6 +12651,18 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -12603,6 +12782,15 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.8", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", @@ -12983,6 +13171,37 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/msgpackr": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.0.tgz", + "integrity": "sha512-I8qXuuALqJe5laEBYoFykChhSXLikZmUhccjGsPuSJ/7uPip2TJ7lwdIQwWSAi0jGZDXv4WOP8Qg65QZRuXxXw==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, "node_modules/mu2": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/mu2/-/mu2-0.5.21.tgz", @@ -13118,7 +13337,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", - "dev": true, "license": "MIT" }, "node_modules/node-addon-api": { @@ -13189,6 +13407,21 @@ "node": ">= 10.12.0" } }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -14743,6 +14976,27 @@ "node": ">= 10.13.0" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -15599,6 +15853,12 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/package.json b/package.json index 1c52a993..ae00e351 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,9 @@ "test": "jest", "test:watch": "cross-env JEST_WATCH=true jest --watch", "db:migrate": "knex migrate:latest", - "db:migrate:make": "knex migrate:make" + "db:migrate:rollback": "knex migrate:rollback", + "db:migrate:make": "knex migrate:make", + "mock:install": "bash scripts/mock_install.sh" }, "workspaces": [ "packages/app", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index aa34ca6b..849366b9 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -2,7 +2,7 @@ import { CssBaseline, ThemeProvider } from '@mui/material' import { StrictMode, useEffect } from 'react' import { createRoot } from 'react-dom/client' import './index.css' -import AppRouter from './routes' +import { AppRouter } from './routes' import '@fontsource/roboto/300.css' import '@fontsource/roboto/400.css' @@ -26,22 +26,19 @@ const THEMES = { dark, } -function AppRender() { +const AppRender = () => { const { prefersDark } = useBrowserPreferences() const [theme] = useThemeStorage() initPosthog() - useEffect( - function syncFavIcon() { - const favicon = document.querySelector( - 'link[rel="icon"]', - ) as HTMLLinkElement | null + useEffect(() => { + const favicon = document.querySelector( + 'link[rel="icon"]', + ) as HTMLLinkElement | null - if (!favicon) return + if (!favicon) return - favicon.href = FAV_ICONS[prefersDark ? 'white' : 'dark'] - }, - [prefersDark], - ) + favicon.href = FAV_ICONS[prefersDark ? 'white' : 'dark'] + }, [prefersDark]) const backupTheme = prefersDark ? 'dark' : 'light' const muiTheme = theme ? THEMES[theme] : THEMES[backupTheme] diff --git a/packages/app/src/api/index.ts b/packages/app/src/api/index.ts new file mode 100644 index 00000000..f5521ddf --- /dev/null +++ b/packages/app/src/api/index.ts @@ -0,0 +1,25 @@ +import { + useQuery, + UseQueryOptions, + UseQueryResult, +} from '@tanstack/react-query' + +export const BASE_API_URL = '/api/v1' + +export const useBetterQuery = ( + options: Omit, 'initialData'> & { + initialData?: () => undefined + }, +): UseQueryResult & { isEmpty: boolean } => { + const queryResult = useQuery(options) + + // Determine if the data is "empty" + const isEmpty = queryResult.data + ? Array.isArray(queryResult.data) + ? queryResult.data.length === 0 + : Object.keys(queryResult.data).length === 0 + : true + + // Return the original query result with the isEmpty property + return { ...queryResult, isEmpty } +} diff --git a/packages/app/src/api/keys.ts b/packages/app/src/api/keys.ts new file mode 100644 index 00000000..96e71243 --- /dev/null +++ b/packages/app/src/api/keys.ts @@ -0,0 +1,20 @@ +export const pulseKeys = { + pulse: ['pulse'] as const, + latestPulses: ['pulse', 'latest-pulses'] as const, + sources: ['sources'] as const, + weekOverview: (date: string) => ['weekOverview', date] as const, + deepWork: (startDate: string, endDate: string) => + ['deepWork', startDate, endDate] as const, + categoryTimeOverview: (startDate: string, endDate: string) => + ['categoryTimeOverview', startDate, endDate] as const, + sourcesMinutes: (startDate: string, endDate: string) => + ['sourcesMinutes', startDate, endDate] as const, + sitesMinutes: (startDate: string, endDate: string) => + ['sitesMinutes', startDate, endDate] as const, + perProjectOverviewTopThree: (startDate: string, endDate: string) => + ['perProjectTimeOverview', 'topThree', startDate, endDate] as const, +} + +export const userKeys = { + user: ['user'] as const, +} diff --git a/packages/app/src/api/pulse.api.ts b/packages/app/src/api/pulse.api.ts new file mode 100644 index 00000000..34cb762b --- /dev/null +++ b/packages/app/src/api/pulse.api.ts @@ -0,0 +1,26 @@ +import { Dayjs } from 'dayjs' +import { useBetterQuery } from '../services' +import { pulseKeys } from './keys' +import { getDeepWorkBetweenDates } from './services/pulse.service' + +interface DeepWorkPeriod { + startDate: string + endDate: string + time: number +} + +const useDeepWorkV2 = (selectedStartDate: Dayjs, selectedEndDate: Dayjs) => { + const startDate = selectedStartDate?.startOf('day').toISOString() + const endDate = selectedEndDate?.endOf('day').toISOString() + + const queryFn = () => + getDeepWorkBetweenDates(selectedStartDate, selectedEndDate) + + return useBetterQuery({ + queryKey: pulseKeys.deepWork(startDate, endDate), + queryFn, + enabled: !!selectedStartDate && !!selectedEndDate, + }) +} + +export { useDeepWorkV2 } diff --git a/packages/app/src/api/repos/pulse.repo.ts b/packages/app/src/api/repos/pulse.repo.ts new file mode 100644 index 00000000..5b45fa13 --- /dev/null +++ b/packages/app/src/api/repos/pulse.repo.ts @@ -0,0 +1,28 @@ +import { deepWorkSql } from './queries/deepWork.query' + +const getLatestPulses = () => { + // Example query + const query = ` + SELECT * + FROM activities_pulse + ORDER BY id DESC + LIMIT 10 + ` + return query +} + +const getAllPulses = () => { + const query = ` + SELECT * + FROM activities_pulse + ORDER BY created_at DESC + ` + return query +} + +const getDeepWork = (startDate: string, endDate: string) => { + const query = deepWorkSql(startDate, endDate) + return query +} + +export { getAllPulses, getLatestPulses, getDeepWork } diff --git a/packages/app/src/api/repos/queries/deepWork.query.ts b/packages/app/src/api/repos/queries/deepWork.query.ts new file mode 100644 index 00000000..87288961 --- /dev/null +++ b/packages/app/src/api/repos/queries/deepWork.query.ts @@ -0,0 +1,36 @@ +export const deepWorkSql = (startDate: string, endDate: string) => ` + WITH get_periods AS ( + select MIN(time) AS interval_start, + COUNT(*) AS activity_count, + (strftime('%s', time) / 120) AS interval_id + from activities_pulse + where time BETWEEN '${startDate}' AND '${endDate}' + group by (strftime('%s', time) / 120) + order by interval_id asc +), + + flagged AS ( + SELECT *, + (strftime('%s', interval_start) - strftime('%s', LAG(interval_start) OVER (ORDER BY interval_start)) <= 240) AS within_4_minutes + FROM get_periods + ), + groups AS ( + SELECT *, + SUM(CASE WHEN within_4_minutes = 0 THEN 1 ELSE 0 END) OVER (ORDER BY interval_start) AS reset_group + FROM flagged + ), + flow_states AS ( + SELECT *, + SUM(within_4_minutes) OVER (PARTITION BY reset_group ORDER BY interval_start) AS cumulative_within_4_minutes + FROM groups + ), + flow_final AS ( + + select reset_group as flow_group, min(interval_start) as flow_start, (max(cumulative_within_4_minutes) + 1) * 2 as flow_time + from flow_states + group by flow_group + ) + +select * from flow_final +where flow_time > 14; +` diff --git a/packages/app/src/api/repos/user.repo.ts b/packages/app/src/api/repos/user.repo.ts new file mode 100644 index 00000000..dc82f447 --- /dev/null +++ b/packages/app/src/api/repos/user.repo.ts @@ -0,0 +1,39 @@ +import { db, sqlWithBindings } from '../../utils/db.util' + +const getCurrentUser = () => { + // Example query + const query = ` + SELECT * + FROM accounts_user + JOIN accounts_user_settings ON accounts_user.id = accounts_user_settings.user_id + LIMIT 1 + ` + return query +} + +const updateUser = (userId: number, user: Partial) => { + const query = db + .updateTable('accounts_user') + .set(user) + .where('id', '=', userId) + .returningAll() + + const sql = sqlWithBindings(query.compile()) + return sql +} + +const updateUserSettings = ( + userId: number, + settings: Partial, +) => { + const query = db + .updateTable('accounts_user_settings') + .set(settings) + .where('user_id', '=', userId) + .returningAll() + + const sql = sqlWithBindings(query.compile()) + return sql +} + +export { getCurrentUser, updateUser, updateUserSettings } diff --git a/packages/app/src/api/services/pulse.service.ts b/packages/app/src/api/services/pulse.service.ts new file mode 100644 index 00000000..8d31f8a7 --- /dev/null +++ b/packages/app/src/api/services/pulse.service.ts @@ -0,0 +1,52 @@ +import { Dayjs } from 'dayjs' +import { getDeepWork } from '../repos/pulse.repo' +import { sqlQueryFn } from './query.service' + +interface DeepWorkPeriod { + startDate: string + endDate: string + time: number +} + +const getDeepWorkBetweenDates = async ( + selectedStartDate: Dayjs, + selectedEndDate: Dayjs, +): Promise => { + const startDate = selectedStartDate?.startOf('day').toISOString() + const endDate = selectedEndDate?.endOf('day').toISOString() + + const deepWorkSql = getDeepWork(startDate, endDate) + + const records: CodeClimbers.DeepWorkTime[] = await sqlQueryFn(deepWorkSql) + + const periods: DeepWorkPeriod[] = [] + let currentPeriod: DeepWorkPeriod | null = null + + const isSameDay = (date1: string, date2: string) => { + return new Date(date1).toDateString() === new Date(date2).toDateString() + } + + records.forEach((item) => { + if (currentPeriod && isSameDay(currentPeriod.startDate, item.flowStart)) { + currentPeriod.endDate = item.flowStart + currentPeriod.time += item.flowTime + } else { + if (currentPeriod) { + periods.push(currentPeriod) + } + currentPeriod = { + startDate: item.flowStart, + endDate: item.flowStart, + time: item.flowTime, + } + } + }) + + if (currentPeriod) { + periods.push(currentPeriod) + } + + return periods +} + +export { getDeepWorkBetweenDates } diff --git a/packages/app/src/services/query.service.ts b/packages/app/src/api/services/query.service.ts similarity index 62% rename from packages/app/src/services/query.service.ts rename to packages/app/src/api/services/query.service.ts index 52709952..c4805cbc 100644 --- a/packages/app/src/services/query.service.ts +++ b/packages/app/src/api/services/query.service.ts @@ -1,5 +1,5 @@ -import { BASE_API_URL } from '.' -import { apiRequest } from '../utils/request' +import { BASE_API_URL } from '../' +import { apiRequest } from '../../utils/request' // do not use this directly in a component const sqlQueryFn = (query: string) => @@ -9,6 +9,4 @@ const sqlQueryFn = (query: string) => body: { query }, }) -export default { - sqlQueryFn, -} +export { sqlQueryFn } diff --git a/packages/app/src/api/user.api.ts b/packages/app/src/api/user.api.ts new file mode 100644 index 00000000..80f90990 --- /dev/null +++ b/packages/app/src/api/user.api.ts @@ -0,0 +1,65 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useBetterQuery } from '.' +import { userKeys } from './keys' +import { + getCurrentUser, + updateUserSettings, + updateUser, +} from './repos/user.repo' +import { sqlQueryFn } from './services/query.service' + +type UserWithSettings = CodeClimbers.User & CodeClimbers.UserSettings +// do not use this directly in a component +const useGetCurrentUser = () => { + const queryFn = async () => { + const sql = getCurrentUser() + const records = await sqlQueryFn(sql) + return records[0] + } + return useBetterQuery({ + queryKey: userKeys.user, + queryFn, + }) +} + +const useUpdateUserSettings = () => { + const queryClient = useQueryClient() + const queryFn = ({ + user_id, + settings, + }: { + user_id: number + settings: Partial + }) => { + const sql = updateUserSettings(user_id, settings) + return sqlQueryFn(sql) + } + return useMutation({ + mutationFn: queryFn, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: userKeys.user }) + }, + }) +} + +const useUpdateUser = () => { + const queryClient = useQueryClient() + const queryFn = ({ + user_id, + user, + }: { + user_id: number + user: Partial + }) => { + const sql = updateUser(user_id, user) + return sqlQueryFn(sql) + } + return useMutation({ + mutationFn: queryFn, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: userKeys.user }) + }, + }) +} + +export { useGetCurrentUser, useUpdateUserSettings, useUpdateUser } diff --git a/packages/app/src/utils/sql.util.ts b/packages/app/src/api/utils/db.util.ts similarity index 50% rename from packages/app/src/utils/sql.util.ts rename to packages/app/src/api/utils/db.util.ts index bf7971ba..d1cce059 100644 --- a/packages/app/src/utils/sql.util.ts +++ b/packages/app/src/api/utils/db.util.ts @@ -1,6 +1,33 @@ -import { CompiledQuery } from 'kysely' +import { + Kysely, + PostgresAdapter, + DummyDriver, + PostgresIntrospector, + PostgresQueryCompiler, + CompiledQuery, +} from 'kysely' -function sqlWithBindings(compiledQuery: CompiledQuery): string { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Database = Record + +const db = new Kysely({ + dialect: { + createAdapter: () => new PostgresAdapter(), + createDriver: () => new DummyDriver(), + createIntrospector: (db) => new PostgresIntrospector(db), + createQueryCompiler: () => new PostgresQueryCompiler(), + }, +}) + +// Extend the CompiledQuery interface +declare module 'kysely' { + interface CompiledQuery { + sqlWithBindings(): string + } +} + +// Implement the sqlWithBindings method +const sqlWithBindings = (compiledQuery: CompiledQuery): string => { let sql = compiledQuery.sql const parameters = compiledQuery.parameters @@ -27,6 +54,4 @@ function sqlWithBindings(compiledQuery: CompiledQuery): string { return sql } -export default { - sqlWithBindings, -} +export { db, sqlWithBindings } diff --git a/packages/app/src/assets/icons/bar_chart.svg b/packages/app/src/assets/icons/bar_chart.svg new file mode 100644 index 00000000..5459e177 --- /dev/null +++ b/packages/app/src/assets/icons/bar_chart.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/icons/block.svg b/packages/app/src/assets/icons/block.svg new file mode 100644 index 00000000..0f98e350 --- /dev/null +++ b/packages/app/src/assets/icons/block.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/icons/boss.png b/packages/app/src/assets/icons/boss.png new file mode 100644 index 0000000000000000000000000000000000000000..3e5c82af0dda07bc93342a44efc7cc5c648b0409 GIT binary patch literal 108036 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,hA{( zoEs%SWb$~PPT8OknYkWb(ofw_zSs%hwwN20;P7-#zH{|*u`{5|2)MBlmY?Ryi_7@v zvGFC3bd3HyI57j_OKY<$yIeR4s>>S z+|T1@x;^}*VH5zImJ>jI>mc_H098pxypC6ut6$fX?&>QK42^&Pr6DuwhWG2=3EmQb zcO4p9h->a71MSGW7+BvNCZ4T4RAngoKJSgD-Wlhm=e~svlwx?LdFm;E!QvTEqa{ua z3j+!(HsfR{XxFX5I>lCB3!9Kj1}JIMS;7M(=CPd$Ys0i37QHkqV|g0!UF-h7^xM&h_E#ibdpvg%3mr z&y^Lk#eYI;wLxwx){p56_EGX`6q)^OHNq5&oyOE=XGLP|RZsm3`PXEu2`8{NbEPl^ z^Xfsn-J#|@z88`k*7u#w`L#ksb@P- z#*?b7KZUWP*!;=!ClRV>@f2af+z**uEmi!pwb@Gdslfo46p$+He6~Gy^Fak!H8h^F z(`>L!`k@6LIM~I_d}z*kQ~I}H4zs}Y*5o_fR02Wqcgy~~H+AVYqR&#}Yn-*X&5dUE zq}QNpj@a|CWmvA(!%UW5MHJ%={NI~6Le2W$?uH1>D#S@c&l#v?Yi;Nl@6X zPoGE&!-K0Zuq|3=ids2WA`T5eQ12r+%6ms5YsNQm+s$v7>2($J`l>I%FJST!=d-{1 zQXwGxf%2*dx8K?$3EywT=nGp53JD@HkVB@EFPO=NUPRYa&;Y1@li?rn15d4z<{cxt z9y4+eW{k?pFIz)0K8v0!Z=Coav#j6BnVs>k=P;hTB2)%L2X!rf+e;K58 z7jr4$294s~czw=<6~^ukayX+yS|D9$6~}X=_A7yfb|M&%UD2mSoQ**TDF9m|GtI+8 z0ERbp;3RigA6WXe^P!HC3?_ zn>v=p!(F;8a(vG}s&|}@lJMoXUBU~Nqv`;ZHF%Lr3dsG1wV3qcF z*fvXic5R|c>H?5odAn*;Dn51Vd_n=0?YFu~clu_bxzMFcEG~%PDu}k}JwteEzHdZKa9>B$ zy`#Ntud3fBB{4p|egt7}-_Vku7zD6rRra)dS9VWczvzmB}X!>rWbpn@IQ-ZDT z!wG!HLUCge+Q9P6E$d}KANAALy3_ijzbAYovEO= z3wDj8W3MhQFZEjp?=gupb%M=6O{3oHaa2@gUc3*|C6WTT8yUv;aaO{v{IC< zsuF5sab$H>i74m)@|aaNTZQ9bR0r99sjMVa=*UvgwzbnP6@}tuVe=i}S>pke{U_Fq z%oYn*1FjUED4Zfl*;}LBP7OU5lVmAf&;`XC|dq4Wz`cH^#ciV(4ooc;vH!t`v4~lbfUmqV4eRtb>@hyei9nQE@92IG(_TGcco4 zt&sqJV0WRSHNB6g)*BF*d{3m$Cl=31%%>V33~&Kvaf-DzVE<5|m8S)+R`L4okuct2 zeE&P94kv?vg3%%y#$3G7fpQB+^IGPLb)fpwZ$spQl(5zDRe)RI`(~`ph3Hdr09<0% z3X0W^?vm52Ftnps((G>30Pjboelv)+Tn3r!dMaITez3`J`fa0|&!oqND1LPOcc*B< zV^2{!L0AO0b#KB=UW@dQuH%Mml|qrT#;wL_E3W~m$z|Kc3tb*}{9RJC(kg_XOAYYt zNOotf%3M)OJjGW-96mp%$vNF~#4zf;fR=mHwVXU#Vn7)16( z7ejtslyIXP9fu?U!xShWceYOCVsSbOSZg5m+em&+IXh2Wz$6c|YOr_5LZbIV@tRi0jXg6lu`V!z$hG$H`CGeKq5$ z$sNBi-Vf2mf%RpRSo(xh9E)*fmwL1Kvex zy9$YXNnbxFb8=#dDWr!PG)AU_OiDKPW!n7W-^6=9YOme12+GE=$F{4eb+Ovc^~WOi zm`-IyYG^H5d5| zU~b&#*us8HHORzy-?;hPdl{Rb$vKEhxq&2tnlyBiVo6os5Tkq@&$2g>Jpwi1M^cii z1~AKH)Hjk7KKYzeauSV^h{beDTg2!#m`}GdJW0{Kei!Mj;Wr|k>%fMi9EG5KWF~+w zDYubjVed=vi)qbLp$*F@7+`We-t#A`yWGvhb!u{sHImRLFK6WMHyF~-_GgooP^Dp3 zr5F7tA^*Gr#)Ug7U{ePd z;NYriS5*QSHcz6&Vv zJB(vY^sN$FuLcX+3)}=!49q|p%;B^TMK!!5xkQ)@`%dLALAOju9DyUY182FTfm65_ zNx~ykoP3GHPGbiQaT}P`4PM%`Fs@LEn%6Nq1CYK|EEkueR?L!z$Ofg2Gi13eux*%3*O=0$XW z?P~b~n);m)?D-V+xN>uRWA$-0;B#G>G*HdxaVl{`@%eHEyk{}I;qUf`nJHHtNi-QJya2}y&m+c+A=9O@APR!sdxCg5k1wQn8 z&#)^-Twy=RBGrOD#&hSHgzdL{pr1@26$wQG7AN$8X!<-p9JEqB>K>d7j02V@zt1?vxvPa$rzo3Is@9VV?PLKPlUa4b z?rUpi5=!)%iu(+NmLTqLtW1*0&ifwO{*UD!>%~u**@8(^=l}7HB~haZxzuG224#-UHGdJe;d;0XsOpDHg zWctmhop<%~8*8L@4_?%-s*hXI-pA#kt7}YYh4I>c7ye}_fbcp&2*D8$U~^F-CEC|D zCVIp88reFK{Uit%1B|6Rp=hmxpsYPqH1?{zguZeLv!Y+qB}Qbjdny66IZwxD47J*~ zj%Qj8%j6{cd7CVJCsEQx-JCx#)Zvq`0`#cR&8iolol^o{LjF|upe_9w)O*d2*<7wb z)NPa0P3vjE!;5{$aqaT5O%RkB85xN(b>aV!s;b9|hDK~M6be7dJcc)feQAEbiZ z3s_gpJuIlDN?{SU(3>SBK{FWgQScvVzk_=q_{iuVj9YgDPMv3&J;n=T#M9@0`=in5N5IT~kp&8-r@HHD%XR)h_}-v#Gs7x5Jd+< z|7NN(m6)igWHZJ1qP%}ZgOZw{R*raK8m=nQU%A1jYsIf>Y(g@F7wjW_Nsh=*#kYAP ziXA*$GvY4VP477$qo}rBx#68yi8QV%l7>i3_4@ec9+^*-?AZ4cz3oTC;S@Y9j(m%L#o#mD zA$;^Gk4%o4cI_K34dT3j9>9HG=Toce`t#!?sHlLMd1K}it%&B_4-G)Y#VlHJj5Lu) z&fmKc{pES2s+>9R%<^|y8y2f%XjF!Fvte_A}X#ieyutfSz^U0)FWIf zQO00s+||yHqHxC@APYLzhO?SN!VKRz`)Enc#57kQeV1IK)+J#hwXAqc;)5@u{S9vi z{bU-~-aE%BBa34QET2DeDt`9q(Fdmv^V?tqj%Z6>k@7anPy!h8Glw#8!^xn;gUv5H;X)ovLd#;Q`2 z1zUpD_=c^Se~R|AQXtp9gHte+Z49Jxs#Kx7H2%bwYCl zVh(%HRDSGi*Q0k$x%r&b-jH*V~{6hL47LSF&_Vsy7i|)PjPR zK*;6_cezB~8IQutxkGlg)Oqy?GxRze)07Ur=cRTea>Nm5#`?NSDH6*u=T)uu^z^hT zYhYt)fY5B{Ox)iOH4GF z36Jq#)PapzwJ>}MTg4RKRj{7_M*a#pFRzn}^}Kg{U^rf^DPk*#-3VI^!# z7=F$z`=NDWmPrZ0PW~ye31_*LoFl;cujDp%1c*E_>Tw~IsZELbQCHPA z1n9M!Rrj|6{eRapELg;L$_hIy7qM+NNgPp7N#Nd1Cjm7Tm9OE|@$9bnPg$IR5H6nz z0k#7YGx{1>wv1*>$EJ9bd6~ahNY15@5xyF~Ns#9xe2>YjUBpu_Q{PW|9`wC8 z3a#`##oQ>Z?34p)Cca~9^`%gKoLd8eR-!BUv$ph)zl4G2jxu z_qso)*6;Ksf0K@oO|Vaz_e-XUbpUH#+Fhnl7(o-nWk(-D#^thmEkn=(HIe<4zuOgY zp28ayHvhV*5{f5P`z=FwF2Lg|s*!K7bBjiuCW($`T!{9rhmJC!B9wM0nw!X4FvzA# zFTw;5H23DE)egj3nwNXb1jWcU0LzagkC!w1x>Xg<>p}`h^W8TyRYV@_P_KKpqigxe zc4f{-ArFW}kZvU3?Rxtu$KO7C>Bn%g@N3+nt`;w^r!l_p8|SPzAk?8D9z_P<`N6wY z9}-x(=$*ZUG~Z>jDGuK2I3Ii-Cr$jg@h?TO{(1xSZ^MN-WuCN-ho-l8#$Je%NbbR( zGmq%*!KaegP+r!jt(VVu)$43|Wk>y`hSk}V^ZU;0WJh7*R{93B?EC)!ggnv!Pu7sx&4l_|=84c;rWo=AgF4|QukIP31fO>qm z6?pmz|8WIS1Jsk_&P4|p`HvP=5p(Omf}aXHsLx}5{PFq!G#dij1LA(7^1y>Cc{3n{1j2~RXM)SoC67fds- z%9nk2EQCFaNfd$npJTs@j{3^|$HAAX?K)D| ze7`;$B`}giDmFt)fX50gmSu8`A?76i{whW_-Z|x zFpSS6_=Q$WvS^jut4dbXy_)DIBg;bzjdE|FjU(Q`g zR72_eVvF9JGFAF$e(*P99oix<^@E^uU|DDl>&u1|+!ZN$N6pbtHFuqa)w!Mir$>^% zl>5R#IrCymdaSxF#KG-Nx~T#11eC@lH4PSl zgeBwdUu+aUTKJH=XSIlSU~KC z;_K5IH#?B(3i>9-U&+OhJ{Sn!(B)OT4P0 zgg1liVWM+&ZcHUyw=nvBHQ5n@hi(jpUcD-=-5qqj@C1lpuA1&9k(8r^d5bKc7%-Ug zlY==2hTguDar;; z=a+=zZ*g_24GBT(;+-Z~+rtJm;+r&(BlBJFXr3Z<)#YiLm)>UiO>mR6JEt0>({nWJmKzJaB%<0hn;O_xILPA=x zXACl1Z2KL~Ok0?BIi1(7*UD2?)d&x-=ucnlOkpTS0tff>#`yvVbx9C|@JhUW|MK_C zG5+IOb_5rFY???>4P7DOY4O(A)I5-`HqrI zUBDXW&YddUAjtJ@Q#KcCLM9h5AyK&kKE(a3=7air*1Kef2rRoty`6cT`^3y1Byx`d z{;p(k49=m@TYn;Y;G2<(j)@`9-Oo83N}43eGTZ%IOE#C!1sj$Y(LtUcT-9PX2rkTc zd?(w9fW3K%_0}6;M?Q@p!-s&osH+r7-ix_C*eo5vMhVUAw{F&-6x$-ed!tZwaAmO@fo}oaj+^<`>}m`)G$32ag+}m{!$(#B)w!WvLH%fACCtX_|K2vNsO?ey1-NnWZa%f9`|gQwIzG zWEAyYEsB@`sXr{gqeUP=08pF3=8DF9+0is(kq-AQv??6|lAkTL3Qs#idw@ zl_2s`Ue)6s{?u}f_TqkKe+UiqEJ{0^+4Sjk!gkcN6)9ttT=O1viwfrL!rl|hIA9O? zp1ql88;$95I1vG{59{qFO1J!j))n)2_-@tXwx*Pt>p5iv``MVvC1qvH{Td5df~W!&*IM)g4*T)j-(CKxPw$ua)%mL(lu*}~ zSt6dbR2r7sQrRe3Ua{XTJ2P~U)RVR$&JZ`wjxN0p<#`fj5Rc54mcW?yDl_nLWJk z1*!KPc9^PyU&5ll$Im}j`FLLe*#i_!vW zEx^yWp88Ll0n@=5o^~L8%H-GYeK0v_V`$V*0^3GuWh(_DW~D1eL%2z#Ng2UzualsH z0%kpNP*b+;D%pDi<4EQO)}zwosvw!zyXG8P(B%yXr( zCJmzbsKK@Ecm-cp7|y(hiOYiAis2rOd{(D(u26Nz&d}zcg(MZaT${RFvJC(QL!!CH zW-u@wfnjmhCp^i%-Q7dROh3`9Nm0bRH`(#|jMp%*ajgY2o-P}wuXd3l&ut8UrDutc zYdcQ9L{lLMKTJw#YEtsis*W%ap8GoQaHVT${XS!8 z?Z#MN_q+vZeV@5yzGrp3u%xQUymn$xlt$aLGeMsees&mfGC zf2QGi?4q7v4Yr_hf1mkC;c>T28K|Jeb3(2kqJZ>NwO@Cgyv%0ciw9$W?T28zZt(gp zE5%q;=~dTR_;s}q0P0?LN*HBjo=8N z$Hkiq?<8mYsZt{TZ&`AplEZ0ugVN!WhD-M!{ug=DH9WY&*nT{qf#OLx(^!uL(GWiv|CVjmUrr2*?$wpUQl5P=qiIQ&Ov|Eo1Wkjq4W| z7k$2zrFEU4Aw&o`rK;6BaFXzUTYTohGe*X?>~uSh>}^hB|A?5taXh>ZT%6I0^TG>z(~B}@*&ln-yUHl!7;$vE>`kA(iy=LVBP z;O=*&U-RC%t0vF|N`%-pMH&hwD9?@~-b+ z8U1N_WWR$5(?|NTgRl2S?ZL!s00SbluB5X@6kGPGe=1`7$ecsNI3Tg+^p7Q`rGrUf zxpoZ#<&hffcIO?Ya9A-&?{H?66a|%r$f+>s#x|vv9&XqC#X;YtNy6~>}w`W};;B|4Tf`}YHGM1ZQusm$r9k0~HB6#yogBSd8tWK7WV@$?q)qV)+*p=Y3c&LFLUj{b#GzJ zEsg;-J^(m`vN2&BQ{rKQaRp!5_@X|QH#R8_*deqA*1nRB6`Kz&(qwubKJ1SRuk+1w z0<<6g66}2`O{?X?@liUqH~upZt}29uXhE^+F~*aAM8t6Ku-dHs)%4rd0x6cu|pbw zRAR0O|A6hg^&6~(M@CfhlohtRo_M?-fjb%RUTKT*#iW0KQA@#m)^U{&(FB2oC;_YE z%>mcparLtPNS5|RppgL3;(RUlN(H&*RIV zB7gb@gjtw;QL;u3*3oayf{Rl)#p zFJAXb~2XWb$q$xrrK-i<&Or#QArNIr{Wti@jNIma>8g(Qny*^KZT9=_S5n zaB=v9C3%#0Uiaan=Q~uPM35i%iz#3bV*$7|Q#tF?#V7~1S!y7p!ug=wLb8PN#Ix-4 zXcCwZ&0PC9XN|3`mTP3J#+TW*$e?fao;qHLFm0>n%Rx!XIB~X$y-7z_7H8_1c2jH_ zwu|#g_yBm0H3<$I5kUlPKX%Ljy^F=&=rM@95RCKn(8f>-OfmjP=Zi1{;Cn%@^xByl z;o^P%E<%}CyF`+`9te(tKQqFu;{JIS;}4l&m*)#gauZ67Urrh)$cu)5v8hMVcsYm< zd4@Kb5Ugu4jiCN-TkGc5CiHHkv%tXfLSAp(ybYOpH0bQn$3r`M=of%$*ZSYO7U8I7 zFN)Rw0E$3$znr}#t4kMzwReo<$b1t&KfK!nDCWgPSt=n{V8Gzm*>p&MWz1j~|}6Q6wP`R8AG>O&v;IXFjjt}FoP_b<$9L4PAH zCbPvi%r~ucnG3LuYjkVe%QVoZQE$5K4qZ$I&$73KvJ5Os!eIkS5!|MGz7*Iv9st7) z89&0p=Anm88a4X$Non=$d5Z@)Hx>Z&(JX`XH_~e4{gAj;7zJyY&2eGtQkXEnih{2~ zHbOg7T-VQBXZowQ)6ht3-+MJESo1nd(z1wvo#&fjn9Q={x(%jA?b`P2fcp5<`)O_N z=63-a)BO}i-D6CEfpmh_D2!D}$%um@D!~B-sdcFPNMMpq-TcSu1B&Dhijg6U%=*^| z^I=OCl?zH35Ab=%YrgB|&71e&9MHKD08Ej6TJ4D6f210D34%yfex_d(TN(&%b;$K< z9gV-Ok(gF>+4OcGfuU*Ldk*I9E%J=G?bFvlQOnM$65rHqfN6j>aZK1kA}izU(iNFU zN`%j*^10h69z)C+zPBS%} z2(?_SL}%N!`P{?b*siV#KwFB5DJ=ogzG0ux!Tte!=Xc(s_u6W9L*cuj+|uauaF|?s z!W}b#!-qK?KR7Oy<#$d;$^LkCUwdF<3r$~4l3ooLU|HH$2F<3z=@7Ls&QmWet#JSrZc12^HT^{6-<8e%(Z*jU09pAgGUqs;22_(^!`+5a9#3&+mdWOlNxadgX-|o;~Rbf16L| z-HifoBc-;hEBi&BbgqpT3+d|u3yb%Gj#1DZv~3I33OEJcTzwL$UPd(|QnCV5%+3LVNv%7>PCBs+X^l54j3Ci&RT!YrcStMk;kT}2D+7gG#O5)Q{a2s@QK&+ z&Nt)ieOUHt$7JSu|B4=qP`&r@(R}aKBfQ09UFO%EuQeQnMzf~UpF|EQ%PTL&(eU%5 zYX+3Rm+ks%;a-aG5p5rTLibgc<=~e}T^omD5Dndyc>k6Dv{1uBHZZQpiZKCQ>ycjv zH^x=zp;d7KhK^0Zc!0YveCe0YYQ2CnX#v<3od5Go;YErTl(pEZ7nnMHx>ygu7FHJc z-M5IGHEL+I0B0%&0)zHw>xdZ`it3p6ete2N7aRavrU3eLh8RI*q@}6_WNA6}0>%{W z8aXS`XU-~A@Y!?$6xqtAf6M|f<}56EtI<&P$y?h7)O$2Vm^Y$_S80dps;f}>FDArc zH-(`*Xd;hczb2M_7u7NIFA6n+FKgRr)3UV$j6crsrccN<;Kd|`mM}!GbX-}nDZ>3W zj6Eo&7=ZIJH@8(AV#n!*na|IpLVD%|fV+3@JmcP#XJDigp~xz^Q3INU2wu43Qa_0= zGpzi<+oO{TD%)i`OGt3sKhcakP#Cw0SwNkDP*t&HY*xMxXn|OpX`Zc^qVq~lxB1wM zU;gsP;Ed6kiUA*}PQJpJB0e^tMJEswS5{u_(D^?CmM zHHjDj7a|cn0B>NW&}S9;+3um)^@qPOX+vCF)v$FM1op2G#PS@Qw_3>-y$c-2d z|GSm;s{8YBEmhfHgw#>Xfr4WFN@}$c`UB}AvpWf=3I9R&rJM^OgX>rJKU)q`4xMOH)xv%lvYVF!DVYy6(erKYrB>pghwwR<9&Y{G0V zxM}Kq&~=+7pK+fbg)>5D#szTNrhoKdI#9Cwu#)=b!&1oDn(`bBs*?8lHubPMp*L`23XZ zXB*agE_xCopy_Hb1jW_=MjD!S&F37)2vn^TL$!|?2rR_WOooy@OMa}~#&t(Jt8|u7 z0?&jCz&O*t!a_f5@Q1!U)J`9N@F@z}_1a&RdVTsUUZ8<}UF&gSb+|WL&J^3>4RB;W z#vD-ec<}APN`yXGOr=rZaejz_p>P+=^WGvJ zzhxYN+-d!kflv@W2Ft`oFf)!hWPpkZtbeohtcJ5HPJ|(laaS!ue@vUMpF5gyaAsTq zJ8uDa@x?pOxT*9EjC3kQ{`n!ORZDUn)yzqu$y5TlU?Une{!<`jo4M#`J}=$d^8pSI zA29fj%q{I)TG|@gl%V&G-usc7WN)8^tbm;s0MPXB+-%Rn7RmeeFRG8fk2ptpGs?;L zFFxeLgknI5%W!wYvStymSdl>H+yaUANZFgY0Y*A^blqebDzHBeJ4!n%0NlNE z=N3l&Z|Xx$`WBxJrEFcVx7~0m!--Ne?|;1@0&J&nn&jTWK0Nu}_dqWB&%V9cWC{ld z2XOD+J^1QZzuKye5$ecXimT!mImdpl{jM4z>x&pOb^o0s=T+sNu-Z2-qn z7&9W)oXlBz(AtC69c29Y9H)ExaOv`;rhBJj9&TLkz4yDR)t87O?@31Gz&@IGPGEiP z`epdr|F8eVxv3_6hIEiJTb$)1x%LG7`~S&*4*%$jztwuhzWHk*D$Q)mz{;hqTSMpl z*U9wYZ1hlHOjFZOQLvELtsoFtt( z0pKdU{q0XkaOAjPjnpKp72rN@!Vc0-#sHi~nctC^>UrK6!EcQOJK-Ye7{7LY&lEr1 zW`HC6>*sk&6O=KL;ivA@l)g{g-;6E@g0IdQ0baOR#9+IwvrVdBGrsn&;{$ARy%4wI z^Z!j*L%{!rj1ZYV$8?w=Ech&svkp;t7A+pfaIiN6!aOvqgAj3> z)&p*am=TTjGsak?^FlXX`tp}Q1v^MPCjemJ-zsH(N7@qk)IcoOW1%B+QHySYRJcg8HGug53I-|N1ZqG&kc|5?yv7)O{0xC$1VIE~{g&`b#!M{)} znOVck8!a_dDi@35Q6fAt^n%R(h>LII2n>9l2LEQ5C;q>5yz40Cv&!UDZ9YeXEiazg zP-n4HJAkiNGI-Y?-w)_itGgyPELvn+*oTHc`+TGoBF zbd8gdR;8W92G}V9U>7R=`C8y3@lxU!_;fl6+B};76USH*_j!DD)T%LuHk6L60Ghyl z(6mXlQ%>6erO`UC*K9|3hTn4G8;Kp9`y2kSIs#J*Gi zOiDx-2#lBzZ}2FDi7II9_z){@A`YM{$(AgftSk8JkpUYiYNaB9+FObInLt21^j~D9 zU{RtJk!%Fyv(^|u^)9=pyeqa^St_DrX~$dyAXRB`d1`~&UJo17!aCPq*y6CwU2w9u z@4oP*+q)DHxLXzgg!?^X+}E2uwLTwjN?r0lp3fmGGMJe?=ku2Z-UFjp-menfStZPP zAE)MJ8?6D}x9Nog>y>}+Uq69KK$@W8U`A^WAangNn2IRcu1Y3?@6JrAu&QDP_W19{ zS3Of~8DY1E3fHjE98+dCRH@lkr`n57=wz*g-ptkYr2|`V@_V`|d0>lRe1n z)L9=2viu-ZF2ZqIcr(X(VAVTAH6!_XIs=iLAD)RB5(Mbp+|6MH$aJb`jNKQ81aRjn zFHCr5rD~QWd^U+#ATTxrtxj6-_61N%V+{$qF&_JT%TvZ%yEsFooTt-t3WvwXK?e47 zciBKuY-f_DWuCd5mWbuXtXUyzT@8NJ{I^TH77(~I0ssd7xyAGboHCsyiFfPn5w|@& zd4#7f_aS%BCD&Gh1%Z(6n7m5}B-oQbl`tj} zj&iSiT40$DA#|OP;I@x^*SEl}Z+)L@Z%#1H6U_DyK><&~JbIvU%Q-g0uz#TN0ikYC zX0sW&pjdE{{%Cf|Jn{B-z@p>Z*gx%VSH;!|BehQD~^gdsW;#bb7x zC?N2u4}S1dumiLc0ssa671=XwieE1_eqFm-F>=T~Bd5I_IM<5%ISNA@5ktOpdN5_+~;TnSk* zha+?&)dUoe1u@;5!DKouT_spELM%|&!qv-H;nKks5?D+d20~AsyisN52@7DV;^tAN z+Mh(EKdN2f{+l@0*rDq`Y4A1M6+cI{lvb^6DO^M!0*)26*=A|8QK+J-zK6CRNm$DS zX-rGi{5qsqvGRPbTUO1A3uq6WhQ)&e0`LCZ&6_vx!70;jSO75aZ%q5!F=7_^%I~}a z-~8t5WI{eXID{*guEGq{o}39bbH300{r!b45Mf%A*0gY!@VLMMf=XA+NiF2g z20wuK)vG>#O>Hu_Ovr+iupl|$c=6SjV0@Mpi1`v$uwEJ@<&SM7i)i%e?*#1(Lg1+j z095KXciR7oH2SKxLq5+Lms<3#{^-a4r|_?T^oNL1pf+Nh=D6nsfa_PU!6T1chtK?t z|2O=LfAKFm+auF0!N`GV)(QK$VL5A!618ZZ=Uqnr+luL*7H-K`fAHV|q|V=*S!Py= zw|>aEs-giK`EJASo8tVNf|-cYoc+LKQZm6!h=nWmp_`oVr!cn6*9S)L^W%d@=#KlT z|E`z3Z?dp?-#IaQ&)T^^Tx~B4j9&%Iix-S+%YBu9WXBl|lec_zrOuU>n)ctDjr;qQ z=kH@9BTJt|Bfyb+H$Z8yHOmw}OA>s^5}=*c#Sj2iVHhkaag%K}-KTe}PX2z@ivSpD z*J)P}0#DxpkY}HPk#>-Hd3)@!M~Tswiy41npjtveK)yD*S$ z@h0091OPTGwg=#aJ(id21}Y)18`sVJb2j_DLy7WYpr{7tcTtkj9Rkg>-Y z#7Xf1$>OrU>k^vSjR+VDY^kJL&q7=JWksL2i(Nh8QAE$U-GQ+_)hz*DEaVIY+#_>X znO5~0T`hn1&;D8Xsh|3f;N_QJhPfMW+>mCo3ArzB-h7H&G;!~ZiaIbL4PCx`r1i<# z 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

y02zqQq$TrjfCtr0_p*@s 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%0Dh*vJhKtihAusd$L)W24pn(zr*} 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 B@ecq1 literal 0 HcmV?d00001 diff --git a/packages/app/src/assets/icons/notification.svg b/packages/app/src/assets/icons/notification.svg new file mode 100644 index 00000000..9c2174c5 --- /dev/null +++ b/packages/app/src/assets/icons/notification.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/app/src/components/ContributorsPage.tsx b/packages/app/src/components/ContributorsPage.tsx index 131be0a9..9cc6f80d 100644 --- a/packages/app/src/components/ContributorsPage.tsx +++ b/packages/app/src/components/ContributorsPage.tsx @@ -1,11 +1,11 @@ import { Box, Typography } from '@mui/material' -import PlainHeader from './common/PlainHeader' -import contributorsService from '../services/contributors.service' import { SimpleInfoCard, SimpleInfoCardProps } from './common/SimpleInfoCard' import Grid2 from '@mui/material/Unstable_Grid2' +import { getContributors } from '../services/contributors.service' +import { PlainHeader } from './common/PlainHeader' export const ContributorsPage = () => { - const contributors = contributorsService.getContributors() + const contributors = getContributors() const contributorCardData: SimpleInfoCardProps[] = contributors.map( (contributor) => ({ title: contributor.name, diff --git a/packages/app/src/components/Extensions/ExtensionDetail.tsx b/packages/app/src/components/Extensions/ExtensionDetail.tsx index 8636c221..f88eb8cb 100644 --- a/packages/app/src/components/Extensions/ExtensionDetail.tsx +++ b/packages/app/src/components/Extensions/ExtensionDetail.tsx @@ -6,7 +6,12 @@ import { Switch, Typography, } from '@mui/material' -import extensionsService, { Extension } from '../../services/extensions.service' +import { + addExtension, + Extension, + isExtensionAdded, + removeExtension, +} from '../../services/extensions.service' import { Logo } from '../common/Logo/Logo' import { useState } from 'react' import { AuthorInfo } from './AuthorInfo' @@ -20,19 +25,17 @@ interface Props { export const ExtensionDetail = ({ extension }: Props) => { const [imageError, setImageError] = useState(false) - const [isAdded, setIsAdded] = useState( - extensionsService.isExtensionAdded(extension.id), - ) + const [isAdded, setIsAdded] = useState(isExtensionAdded(extension.id)) const handleToggle = () => { if (!isAdded) { posthog.capture(`extension_add_${extension.id}_click`) - extensionsService.addExtension(extension.id) + addExtension(extension.id) extension.onAdd?.() window.location.reload() // need to reload the page so that the app router picks up new extension } else { posthog.capture(`extension_remove_${extension.id}_click`) - extensionsService.removeExtension(extension.id) + removeExtension(extension.id) extension.onRemove?.() } setIsAdded(!isAdded) diff --git a/packages/app/src/components/Extensions/ExtensionsDashboard.tsx b/packages/app/src/components/Extensions/ExtensionsDashboard.tsx index 77d1a4a4..557bc93a 100644 --- a/packages/app/src/components/Extensions/ExtensionsDashboard.tsx +++ b/packages/app/src/components/Extensions/ExtensionsDashboard.tsx @@ -1,13 +1,14 @@ import { Box, Card, CardContent } from '@mui/material' -import extensionsService, { - DashboardExtension, -} from '../../services/extensions.service' import Grid2 from '@mui/material/Unstable_Grid2' -import CodeClimbersButton from '../common/CodeClimbersButton' import { useNavigate } from 'react-router-dom' +import { + getActiveDashboardExtensions, + DashboardExtension, +} from '../../services/extensions.service' +import { CodeClimbersButton } from '../common/CodeClimbersButton' export const ExtensionsDashboard = () => { - const extensions = extensionsService.getActiveDashboardExtensions() + const extensions = getActiveDashboardExtensions() const navigate = useNavigate() const ExtensionCard = ({ extension }: { extension: DashboardExtension }) => { diff --git a/packages/app/src/components/Extensions/ExtensionsPage.tsx b/packages/app/src/components/Extensions/ExtensionsPage.tsx index 4a56caf6..4a3342c1 100644 --- a/packages/app/src/components/Extensions/ExtensionsPage.tsx +++ b/packages/app/src/components/Extensions/ExtensionsPage.tsx @@ -1,10 +1,10 @@ import { Box } from '@mui/material' -import extensionsService from '../../services/extensions.service' import { ExtensionDetail } from './ExtensionDetail' -import PlainHeader from '../common/PlainHeader' +import { PlainHeader } from '../common/PlainHeader' +import { getExtensions } from '../../services/extensions.service' export const ExtensionsPage = () => { - const extensions = extensionsService.extensions + const extensions = getExtensions() return ( { const theme = useTheme() - const { isLoading, isError, data: deepWork } = useDeepWork(selectedDate) + const { + isLoading, + isError, + data: deepWork, + } = useDeepWorkV2(selectedDate, selectedDate.endOf('day')) if (isLoading) return if (isError || !deepWork) return

Error
- const totalTime = deepWork.reduce((acc, curr) => acc + curr.flowTime, 0) + + const totalTime = deepWork[0]?.time ?? 0 return ( <> { ) } -export default DeepWork +export { DeepWork } diff --git a/packages/app/src/components/Home/Extensions/ExtensionsWidget.tsx b/packages/app/src/components/Home/Extensions/ExtensionsWidget.tsx index b7acfa7c..342cacf1 100644 --- a/packages/app/src/components/Home/Extensions/ExtensionsWidget.tsx +++ b/packages/app/src/components/Home/Extensions/ExtensionsWidget.tsx @@ -2,14 +2,17 @@ import { Box, Card, CardContent, Stack, Typography } from '@mui/material' import GitHubIcon from '@mui/icons-material/GitHub' import Grid2 from '@mui/material/Unstable_Grid2/Grid2' import { DiscordIcon } from '../../common/Icons/DiscordIcon' -import CodeClimbersIconButton from '../../common/CodeClimbersIconButton' -import extensionsService from '../../../services/extensions.service' -import CodeClimbersButton from '../../common/CodeClimbersButton' -import contributorsService from '../../../services/contributors.service' import { SimpleInfoCard, SimpleInfoCardProps, } from '../../common/SimpleInfoCard' +import { getSpotlight } from '../../../services/contributors.service' +import { + getNewestExtension, + getPopularExtension, +} from '../../../services/extensions.service' +import { CodeClimbersIconButton } from '../../common/CodeClimbersIconButton' +import { CodeClimbersButton } from '../../common/CodeClimbersButton' const RESOURCES = [ { @@ -24,11 +27,11 @@ const RESOURCES = [ }, ] -const spotlightContributor = contributorsService.getSpotlight() +const spotlightContributor = getSpotlight() export const ExtensionsWidget = () => { - const newestExtension = extensionsService.getNewestExtension() - const popularExtension = extensionsService.getPopularExtension() + const newestExtension = getNewestExtension() + const popularExtension = getPopularExtension() const extensionCardData: SimpleInfoCardProps[] = [] if (popularExtension) { diff --git a/packages/app/src/components/Home/Header.tsx b/packages/app/src/components/Home/HomeHeader.tsx similarity index 97% rename from packages/app/src/components/Home/Header.tsx rename to packages/app/src/components/Home/HomeHeader.tsx index 8d8927ce..b70d9dec 100644 --- a/packages/app/src/components/Home/Header.tsx +++ b/packages/app/src/components/Home/HomeHeader.tsx @@ -17,8 +17,8 @@ import { LightMode, } from '@mui/icons-material' import { useThemeStorage } from '../../hooks/useBrowserStorage' -import CodeClimbersButton from '../common/CodeClimbersButton' import { useNavigate } from 'react-router-dom' +import { CodeClimbersButton } from '../common/CodeClimbersButton' const Header = styled('div')(({ theme }) => ({ display: 'flex', @@ -155,4 +155,4 @@ const HomeHeader = ({ selectedDate, setSelectedDate }: Props) => { ) } -export default HomeHeader +export { HomeHeader } diff --git a/packages/app/src/components/Home/HomePage.tsx b/packages/app/src/components/Home/HomePage.tsx index f8acf57a..c3de251e 100644 --- a/packages/app/src/components/Home/HomePage.tsx +++ b/packages/app/src/components/Home/HomePage.tsx @@ -3,12 +3,12 @@ import { Box } from '@mui/material' import dayjs from 'dayjs' import { Time } from './Time/Time' -import Sources from './Source/Sources' -import HomeHeader from './Header' import { Navigate } from 'react-router-dom' import { useGetHealth } from '../../services/health.service' import { ExtensionsDashboard } from '../Extensions/ExtensionsDashboard' import { ExtensionsWidget } from './Extensions/ExtensionsWidget' +import { Sources } from './Source/Sources' +import { HomeHeader } from './HomeHeader' const HomePage = () => { const { data: health, isPending: isHealthPending } = useGetHealth({ @@ -50,4 +50,4 @@ const HomePage = () => { ) } -export default HomePage +export { HomePage } diff --git a/packages/app/src/components/Home/Source/AddSources.tsx b/packages/app/src/components/Home/Source/AddSources.tsx index 51159f8b..deb1590c 100644 --- a/packages/app/src/components/Home/Source/AddSources.tsx +++ b/packages/app/src/components/Home/Source/AddSources.tsx @@ -12,7 +12,6 @@ import { AccordionActions, } from '@mui/material' import CloseIcon from '@mui/icons-material/Close' -import { useGetSources } from '../../../services/pulse.service' import { AppDetails, SourceDetails, @@ -21,8 +20,9 @@ import { import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import { Refresh } from '@mui/icons-material' import { getTimeSince } from '../../../utils/time' -import CodeClimbersButton from '../../common/CodeClimbersButton' -import CodeClimbersIconButton from '../../common/CodeClimbersIconButton' +import { useGetSources } from '../../../services/pulse.service' +import { CodeClimbersButton } from '../../common/CodeClimbersButton' +import { CodeClimbersIconButton } from '../../common/CodeClimbersIconButton' interface AddSourcesRowProps { source: SourceDetails @@ -176,4 +176,4 @@ const AddSources = ({ open, handleClose }: AddSourcesProps) => { ) } -export default AddSources +export { AddSources } diff --git a/packages/app/src/components/Home/Source/Sources.empty.tsx b/packages/app/src/components/Home/Source/Sources.empty.tsx index b61cc48c..988dc7a2 100644 --- a/packages/app/src/components/Home/Source/Sources.empty.tsx +++ b/packages/app/src/components/Home/Source/Sources.empty.tsx @@ -1,9 +1,9 @@ import { Card, CardContent, Stack, Typography, useTheme } from '@mui/material' import AddIcon from '@mui/icons-material/Add' -import AddSources from './AddSources' import { useState } from 'react' import { rgbAnimatedBorder } from '../../../utils/style/rgbAnimation' -import CodeClimbersButton from '../../common/CodeClimbersButton' +import { AddSources } from './AddSources' +import { CodeClimbersButton } from '../../common/CodeClimbersButton' const SourcesEmpty = () => { const [addSourcesOpen, setAddSourcesOpen] = useState(false) @@ -62,4 +62,4 @@ const SourcesEmpty = () => { ) } -export default SourcesEmpty +export { SourcesEmpty } diff --git a/packages/app/src/components/Home/Source/Sources.error.tsx b/packages/app/src/components/Home/Source/Sources.error.tsx index 058f9f32..e4bec99b 100644 --- a/packages/app/src/components/Home/Source/Sources.error.tsx +++ b/packages/app/src/components/Home/Source/Sources.error.tsx @@ -23,4 +23,4 @@ const SourcesError = () => { ) } -export default SourcesError +export { SourcesError } diff --git a/packages/app/src/components/Home/Source/Sources.loading.tsx b/packages/app/src/components/Home/Source/Sources.loading.tsx index acfec469..56599156 100644 --- a/packages/app/src/components/Home/Source/Sources.loading.tsx +++ b/packages/app/src/components/Home/Source/Sources.loading.tsx @@ -7,7 +7,7 @@ import { CircularProgress, } from '@mui/material' import AddIcon from '@mui/icons-material/Add' -import CodeClimbersButton from '../../common/CodeClimbersButton' +import { CodeClimbersButton } from '../../common/CodeClimbersButton' const SourcesLoading = () => { const theme = useTheme() @@ -51,4 +51,4 @@ const SourcesLoading = () => { ) } -export default SourcesLoading +export { SourcesLoading } diff --git a/packages/app/src/components/Home/Source/Sources.tsx b/packages/app/src/components/Home/Source/Sources.tsx index 4dc9771d..f64c9b0b 100644 --- a/packages/app/src/components/Home/Source/Sources.tsx +++ b/packages/app/src/components/Home/Source/Sources.tsx @@ -5,20 +5,21 @@ import SaveAltOutlinedIcon from '@mui/icons-material/SaveAltOutlined' import AddIcon from '@mui/icons-material/Add' import { Dayjs } from 'dayjs' +import { supportedSources } from '../../../utils/supportedSources' +import { supportedSites } from '../../../utils/supportedSites' +import { SiteRow } from './SiteRow' +import { SourceRow } from './SourceRow' +import { AddSources } from './AddSources' import { useExportPulses, useGetSitesWithMinutes, useGetSourcesWithMinutes, } from '../../../services/pulse.service' -import { AppDetails, supportedSources } from '../../../utils/supportedSources' -import SourcesEmpty from './Sources.empty' -import SourcesError from './Sources.error' -import SourcesLoading from './Sources.loading' -import AddSources from './AddSources' -import { supportedSites } from '../../../utils/supportedSites' -import { SiteRow } from './SiteRow' -import { SourceRow } from './SourceRow' -import CodeClimbersButton from '../../common/CodeClimbersButton' +import { SourcesEmpty } from './Sources.empty' +import { SourcesError } from './Sources.error' +import { SourcesLoading } from './Sources.loading' +import { CodeClimbersButton } from '../../common/CodeClimbersButton' +import { AppDetails } from '../../../utils/supportedSources' type SourcesProps = { selectedDate: Dayjs } const Sources = ({ selectedDate }: SourcesProps) => { @@ -181,4 +182,4 @@ const Sources = ({ selectedDate }: SourcesProps) => { ) } -export default Sources +export { Sources } diff --git a/packages/app/src/components/Home/Time/CategoryChart.tsx b/packages/app/src/components/Home/Time/CategoryChart.tsx index f6cb3cce..2f9d7c67 100644 --- a/packages/app/src/components/Home/Time/CategoryChart.tsx +++ b/packages/app/src/components/Home/Time/CategoryChart.tsx @@ -12,8 +12,8 @@ import { TimeDataChart } from './TimeDataChart' import { minutesToHours } from './utils' import { useCategoryTimeOverview, - usePerProjectOverviewTopThree, useWeekOverview, + usePerProjectOverviewTopThree, } from '../../../services/pulse.service' const categories = { @@ -141,4 +141,4 @@ const CategoryChart = ({ selectedDate }: Props) => { ) } -export default CategoryChart +export { CategoryChart } diff --git a/packages/app/src/components/Home/Time/Time.tsx b/packages/app/src/components/Home/Time/Time.tsx index c6d0df66..a3c1434a 100644 --- a/packages/app/src/components/Home/Time/Time.tsx +++ b/packages/app/src/components/Home/Time/Time.tsx @@ -1,12 +1,44 @@ -import { Card, CardContent, Divider, Typography } from '@mui/material' +import { Box, Card, CardContent, Divider, Typography } from '@mui/material' import Grid2 from '@mui/material/Unstable_Grid2/Grid2' import { Dayjs } from 'dayjs' -import CategoryChart from './CategoryChart' -import DeepWork from '../DeepWork' +import { BossImage } from '../../common/Icons/BossImage' +import { useState } from 'react' +import { WeeklyReportDialog } from '../../common/WeeklyReportDialog' +import { NotificationIcon } from '../../common/Icons/NotificationIcon' +import { DeepWork } from '../DeepWork' +import { CategoryChart } from './CategoryChart' +import { useGetCurrentUser } from '../../../api/user.api' type Props = { selectedDate: Dayjs } export const Time = ({ selectedDate }: Props) => { + const [isWeeklyReportModalOpen, setIsWeeklyReportModalOpen] = useState(false) + // const { data: user } = useGetCurrentUser() + // const WeeklyReportSettings = () => { + // const showNotificationIcon = user?.weeklyReportType === '' && !user?.email + // return ( + // { + // setIsWeeklyReportModalOpen(true) + // }} + // > + // + // {showNotificationIcon && ( + // + // )} + // + // ) + // } + return ( { Time + {/* + + */} + {/* {user && isWeeklyReportModalOpen && ( + setIsWeeklyReportModalOpen(false)} + /> + )} */} ) } diff --git a/packages/app/src/components/ImportPage.tsx b/packages/app/src/components/ImportPage.tsx index d7793566..574a2c3f 100644 --- a/packages/app/src/components/ImportPage.tsx +++ b/packages/app/src/components/ImportPage.tsx @@ -1,12 +1,12 @@ import { Box, Card, CardContent, TextField, Typography } from '@mui/material' -import CodeClimbersButton from './common/CodeClimbersButton' import { Logo } from './common/Logo/Logo' import { useNavigate } from 'react-router-dom' import { CodeSnippit } from './common/CodeSnippit/CodeSnippit' -import CodeClimbersLoadingButton from './common/CodeClimbersLoadingButton' import { useValidateLocalApiKey } from '../services/localAuth.service' import { useState } from 'react' -import authUtil from '../utils/auth.util' +import { setLocalApiKey } from '../utils/auth.util' +import { CodeClimbersButton } from './common/CodeClimbersButton' +import { CodeClimbersLoadingButton } from './common/CodeClimbersLoadingButton' export const ImportPage = () => { const navigate = useNavigate() @@ -28,7 +28,7 @@ export const ImportPage = () => { } const handleSubmit = async () => { - authUtil.setLocalApiKey(apiKey) + setLocalApiKey(apiKey) setHasSubmitted(true) refetch() } diff --git a/packages/app/src/components/InstallPage.tsx b/packages/app/src/components/InstallPage.tsx index 9a3532f7..8936d807 100644 --- a/packages/app/src/components/InstallPage.tsx +++ b/packages/app/src/components/InstallPage.tsx @@ -1,7 +1,6 @@ import { Box, Typography, Paper, CircularProgress, Link } from '@mui/material' import { DiscordIcon } from './common/Icons/DiscordIcon' import GitHubIcon from '@mui/icons-material/GitHub' -import CodeClimbersButton from './common/CodeClimbersButton' import { Logo } from './common/Logo/Logo' import { CodeSnippit } from './common/CodeSnippit/CodeSnippit' import { useState } from 'react' @@ -9,6 +8,7 @@ import { isMobile } from '../../../server/utils/environment.util' import installBackground from '@app/assets/background_install.png' import { Navigate } from 'react-router-dom' import { useGetHealth } from '../services/health.service' +import { CodeClimbersButton } from './common/CodeClimbersButton' const InstallPage = () => { const [isWaiting, setIsWaiting] = useState(false) @@ -248,4 +248,4 @@ const InstallPage = () => { ) } -export default InstallPage +export { InstallPage } diff --git a/packages/app/src/components/UpdatePage.tsx b/packages/app/src/components/UpdatePage.tsx index 290dcb29..a671b9ba 100644 --- a/packages/app/src/components/UpdatePage.tsx +++ b/packages/app/src/components/UpdatePage.tsx @@ -1,9 +1,9 @@ import { Box, Card, CardContent, Typography } from '@mui/material' -import CodeClimbersButton from './common/CodeClimbersButton' import { Logo } from './common/Logo/Logo' import { useNavigate } from 'react-router-dom' import { useUpdateVersionHook } from '../hooks/useUpdateHook' import { CodeSnippit } from './common/CodeSnippit/CodeSnippit' +import { CodeClimbersButton } from './common/CodeClimbersButton' // Used to display when an update is not just available, but required. export const UpdatePage = () => { diff --git a/packages/app/src/components/common/CodeClimbersButton.tsx b/packages/app/src/components/common/CodeClimbersButton.tsx index eec1d732..ae186476 100644 --- a/packages/app/src/components/common/CodeClimbersButton.tsx +++ b/packages/app/src/components/common/CodeClimbersButton.tsx @@ -25,4 +25,4 @@ const CodeClimbersButton = ({ ) } -export default CodeClimbersButton +export { CodeClimbersButton } diff --git a/packages/app/src/components/common/CodeClimbersIconButton.tsx b/packages/app/src/components/common/CodeClimbersIconButton.tsx index 1bd61424..c66028a9 100644 --- a/packages/app/src/components/common/CodeClimbersIconButton.tsx +++ b/packages/app/src/components/common/CodeClimbersIconButton.tsx @@ -26,4 +26,4 @@ const CodeClimbersIconButton = ({ ) } -export default CodeClimbersIconButton +export { CodeClimbersIconButton } diff --git a/packages/app/src/components/common/CodeClimbersLoadingButton.tsx b/packages/app/src/components/common/CodeClimbersLoadingButton.tsx index dc34105d..680c9c35 100644 --- a/packages/app/src/components/common/CodeClimbersLoadingButton.tsx +++ b/packages/app/src/components/common/CodeClimbersLoadingButton.tsx @@ -26,4 +26,4 @@ const CodeClimbersLoadingButton = ({ ) } -export default CodeClimbersLoadingButton +export { CodeClimbersLoadingButton } diff --git a/packages/app/src/components/common/CodeSnippit/CodeSnippit.tsx b/packages/app/src/components/common/CodeSnippit/CodeSnippit.tsx index b04d9177..abce81ac 100644 --- a/packages/app/src/components/common/CodeSnippit/CodeSnippit.tsx +++ b/packages/app/src/components/common/CodeSnippit/CodeSnippit.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from 'react' import Grid2 from '@mui/material/Unstable_Grid2/Grid2' import { Check, ContentCopy, Error } from '@mui/icons-material' -import CodeClimbersIconButton from '../CodeClimbersIconButton' import { Box } from '@mui/material' +import { CodeClimbersIconButton } from '../CodeClimbersIconButton' const timers: NodeJS.Timeout[] = [] diff --git a/packages/app/src/components/common/Icons/BarChartIcon.tsx b/packages/app/src/components/common/Icons/BarChartIcon.tsx new file mode 100644 index 00000000..61e87f3f --- /dev/null +++ b/packages/app/src/components/common/Icons/BarChartIcon.tsx @@ -0,0 +1,17 @@ +import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon' + +// Thank you! https://github.com/mui/material-ui/issues/35218#issuecomment-1977984142 +export const BarChartIcon = (props: SvgIconProps) => { + return ( + + + + ) +} diff --git a/packages/app/src/components/common/Icons/BlockIcon.tsx b/packages/app/src/components/common/Icons/BlockIcon.tsx new file mode 100644 index 00000000..dfa3bd8a --- /dev/null +++ b/packages/app/src/components/common/Icons/BlockIcon.tsx @@ -0,0 +1,15 @@ +import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon' + +// Thank you! https://github.com/mui/material-ui/issues/35218#issuecomment-1977984142 +export const BlockIcon = (props: SvgIconProps) => ( + + + +) diff --git a/packages/app/src/components/common/Icons/BossImage.tsx b/packages/app/src/components/common/Icons/BossImage.tsx new file mode 100644 index 00000000..9e6890d1 --- /dev/null +++ b/packages/app/src/components/common/Icons/BossImage.tsx @@ -0,0 +1,11 @@ +import bossImage from '@app/assets/icons/boss.png' +type Props = { + width?: number + height?: number +} & React.ImgHTMLAttributes + +export const BossImage = ({ width = 32, height = 32, ...props }: Props) => { + return ( + Boss + ) +} diff --git a/packages/app/src/components/common/Icons/NotificationIcon.tsx b/packages/app/src/components/common/Icons/NotificationIcon.tsx new file mode 100644 index 00000000..cdc4f72b --- /dev/null +++ b/packages/app/src/components/common/Icons/NotificationIcon.tsx @@ -0,0 +1,19 @@ +import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon' + +// Thank you! https://github.com/mui/material-ui/issues/35218#issuecomment-1977984142 +export const NotificationIcon = (props: SvgIconProps) => ( + + + + +) diff --git a/packages/app/src/components/common/LocalApiKeyErrorBanner.tsx b/packages/app/src/components/common/LocalApiKeyErrorBanner.tsx index 53b2db1c..6e740ee6 100644 --- a/packages/app/src/components/common/LocalApiKeyErrorBanner.tsx +++ b/packages/app/src/components/common/LocalApiKeyErrorBanner.tsx @@ -1,7 +1,7 @@ import { Alert, Box } from '@mui/material' -import CodeClimbersButton from './CodeClimbersButton' import { useValidateLocalApiKey } from '../../services/localAuth.service' import { useNavigate } from 'react-router-dom' +import { CodeClimbersButton } from './CodeClimbersButton' export const LocalApiKeyErrorBanner = () => { const { data, isPending } = useValidateLocalApiKey('banner') diff --git a/packages/app/src/components/common/PlainHeader.tsx b/packages/app/src/components/common/PlainHeader.tsx index 004ae14d..15ee3211 100644 --- a/packages/app/src/components/common/PlainHeader.tsx +++ b/packages/app/src/components/common/PlainHeader.tsx @@ -1,9 +1,9 @@ import { Box, Typography } from '@mui/material' import { Logo } from '../common/Logo/Logo' -import CodeClimbersButton from '../common/CodeClimbersButton' import { useNavigate } from 'react-router-dom' import { Close } from '@mui/icons-material' +import { CodeClimbersButton } from './CodeClimbersButton' type Props = { title: string @@ -48,4 +48,4 @@ const PlainHeader = ({ title }: Props) => { ) } -export default PlainHeader +export { PlainHeader } diff --git a/packages/app/src/components/common/WeeklyReportDialog.tsx b/packages/app/src/components/common/WeeklyReportDialog.tsx new file mode 100644 index 00000000..c87fe706 --- /dev/null +++ b/packages/app/src/components/common/WeeklyReportDialog.tsx @@ -0,0 +1,216 @@ +import { + Box, + Dialog, + Typography, + TextField, + Divider, + Card, +} from '@mui/material' +import CloseIcon from '@mui/icons-material/Close' +import CodeClimbersButton from './CodeClimbersButton' +import CodeClimbersIconButton from './CodeClimbersIconButton' +import { useState } from 'react' + +import { BossImage } from './Icons/BossImage' +import { BarChartIcon } from './Icons/BarChartIcon' +import { BlockIcon } from './Icons/BlockIcon' +import userApi from '../../api/user.api' +import { NotificationIcon } from './Icons/NotificationIcon' + +interface ReportOption { + type: CodeClimbers.WeeklyReportType + img: () => React.ReactNode + name: string +} + +const ReportOptions: ReportOption[] = [ + { + type: 'ai', + img: () => , + name: 'Big Brother Edition', + }, + { + type: 'standard', + img: () => , + name: 'Standard', + }, + { + type: 'none', + img: () => , + name: 'None', + }, +] + +const ReportOptionCard = ({ + selected, + recordOption, + onClick, +}: { + selected: boolean + recordOption: ReportOption + onClick: () => void +}) => { + const { img, name } = recordOption + + return ( + + + {img()} + {name} + + + ) +} + +export const WeeklyReportDialog = ({ + open, + onClose, + user, +}: { + open: boolean + user: CodeClimbers.User & CodeClimbers.UserSettings + onClose: () => void +}) => { + const { mutate: updateUserSettings } = userApi.useUpdateUserSettings() + const { mutate: updateUser } = userApi.useUpdateUser() + + const handleClose = () => { + onClose() + setReportOption(user.weeklyReportType || '') + } + + const [reportOption, setReportOption] = + useState(user.weeklyReportType) + const [email, setEmail] = useState(user.email || '') + + const handleOptionClick = (option: CodeClimbers.WeeklyReportType) => { + setReportOption(option) + } + + const handleSave = () => { + if (!user.id) return + updateUserSettings({ + user_id: user.id, + settings: { + weekly_report_type: reportOption, + }, + }) + updateUser({ + user_id: user?.id, + user: { + email: email, + }, + }) + handleClose() + } + const showNotificationIcon = reportOption === '' || !email + + return ( + + + + + Weekly Report + + + + + {showNotificationIcon && ( + + + + Choose an option for a weekly email report of your coding stats. + + + )} + + + + {ReportOptions.map((option) => ( + handleOptionClick(option.type)} + /> + ))} + + + setEmail(e.target.value)} + fullWidth + inputProps={{ + 'data-lpignore': 'true', + 'data-form-type': 'other', + }} + /> + + + Save + + + + ) +} diff --git a/packages/app/src/config/theme.ts b/packages/app/src/config/theme.ts index e42ec3dc..4c6e51a7 100644 --- a/packages/app/src/config/theme.ts +++ b/packages/app/src/config/theme.ts @@ -111,7 +111,7 @@ const darkOptions: ThemeOptions = { mode: 'dark', background: { default: '#1D1D1D', - paper: '#262626', + paper: BASE_THEME_GREYS[900], paper_raised: '#323232', medium: '#3F3F3F', border: '#707070', diff --git a/packages/app/src/extensions/SqlSandbox/SqlSandbox.tsx b/packages/app/src/extensions/SqlSandbox/SqlSandbox.tsx index 77b08921..abb0451b 100644 --- a/packages/app/src/extensions/SqlSandbox/SqlSandbox.tsx +++ b/packages/app/src/extensions/SqlSandbox/SqlSandbox.tsx @@ -1,10 +1,10 @@ import { Box, Typography, useTheme } from '@mui/material' -import CodeClimbersButton from '../../components/common/CodeClimbersButton' import { useNavigate } from 'react-router-dom' import AddIcon from '@mui/icons-material/Add' -import sqlSaveService from './sqlSandbox.service' +import { getSqlList } from './sqlSandbox.service' +import { CodeClimbersButton } from '../../components/common/CodeClimbersButton' -export default function SqlSandbox() { +export const SqlSandbox = () => { const navigate = useNavigate() const theme = useTheme() const handleAddClick = () => { @@ -37,7 +37,7 @@ export default function SqlSandbox() { Saved Queries - {sqlSaveService.getSqlList().map((sql) => ( + {getSqlList().map((sql) => ( { // get sql name from url param const [searchParams] = useSearchParams() const sqlIdFromUrl = searchParams.get('sqlId') const navigate = useNavigate() - const savedSql = sqlSaveService.getSql(sqlIdFromUrl || '') || defaultSql + const savedSql = getSql(sqlIdFromUrl || '') || defaultSql const [sql, setSql] = useState(savedSql.sql || defaultSql) const [sqlName, setSqlName] = useState(savedSql.name || 'Untitled.sql') @@ -49,14 +49,14 @@ export default function SqlSandboxPage() { } const onDownloadCsv = () => { - const csvContent = csvUtil.convertRecordsToCSV(results) + const csvContent = convertRecordsToCSV(results) const blob = new Blob([csvContent]) - csvUtil.downloadBlob(blob, 'results.csv') + downloadBlob(blob, 'results.csv') } const onSaveSql = () => { // save sql to local storage - sqlSaveService.saveSql(sqlName, sql, sqlId) + saveSql(sqlName, sql, sqlId) setSqlId(sqlId) } @@ -66,7 +66,7 @@ export default function SqlSandboxPage() { const onDeleteSql = () => { if (!sqlIdFromUrl) return - sqlSaveService.deleteSql(sqlIdFromUrl) + deleteSql(sqlIdFromUrl) navigate('/') } diff --git a/packages/app/src/extensions/SqlSandbox/index.tsx b/packages/app/src/extensions/SqlSandbox/index.tsx index f50e454c..26c84aba 100644 --- a/packages/app/src/extensions/SqlSandbox/index.tsx +++ b/packages/app/src/extensions/SqlSandbox/index.tsx @@ -1,3 +1 @@ -import SqlSandbox from './SqlSandbox' - -export default SqlSandbox +export { SqlSandbox } from './SqlSandbox' diff --git a/packages/app/src/extensions/SqlSandbox/sandbox.api.ts b/packages/app/src/extensions/SqlSandbox/sandbox.api.ts index 99da0528..a62aeabe 100644 --- a/packages/app/src/extensions/SqlSandbox/sandbox.api.ts +++ b/packages/app/src/extensions/SqlSandbox/sandbox.api.ts @@ -1,8 +1,8 @@ import { useMutation } from '@tanstack/react-query' -import queryApi from '../../services/query.service' +import { sqlQueryFn } from '../../api/services/query.service' export const useRunSql = () => { return useMutation({ - mutationFn: (query: string) => queryApi.sqlQueryFn(query), + mutationFn: (query: string) => sqlQueryFn(query), }) } diff --git a/packages/app/src/extensions/SqlSandbox/sqlSandbox.service.ts b/packages/app/src/extensions/SqlSandbox/sqlSandbox.service.ts index 15e66cd5..bd34ab23 100644 --- a/packages/app/src/extensions/SqlSandbox/sqlSandbox.service.ts +++ b/packages/app/src/extensions/SqlSandbox/sqlSandbox.service.ts @@ -72,4 +72,4 @@ ORDER BY count(language) DESC; } } -export default { saveSql, getSql, deleteSql, getSqlList, onAdd } +export { saveSql, getSql, deleteSql, getSqlList, onAdd } diff --git a/packages/app/src/hooks/useBrowserStorage.ts b/packages/app/src/hooks/useBrowserStorage.ts index d363fd9b..51468026 100644 --- a/packages/app/src/hooks/useBrowserStorage.ts +++ b/packages/app/src/hooks/useBrowserStorage.ts @@ -10,7 +10,7 @@ type LocalStorageOptions = { type SetValueArg = T | ((prev: T) => T) -export function useBrowserStorage(options: LocalStorageOptions) { +export const useBrowserStorage = (options: LocalStorageOptions) => { const storage = typeof window === 'undefined' ? undefined @@ -64,7 +64,7 @@ export function useBrowserStorage(options: LocalStorageOptions) { } } - useEffect(function storageListener() { + useEffect(() => { const abortController = new AbortController() syncStorage() window.addEventListener('storage', subscribeToStorage, { diff --git a/packages/app/src/hooks/useUpdateHook.ts b/packages/app/src/hooks/useUpdateHook.ts index aec5d09a..c2688593 100644 --- a/packages/app/src/hooks/useUpdateHook.ts +++ b/packages/app/src/hooks/useUpdateHook.ts @@ -1,6 +1,6 @@ import { useGetLocalVersion } from '../services/health.service' import { useLatestVersion } from '../services/version.service' -import environmentUtil from '../utils/environment.util' +import { extractVersions } from '../utils/environment.util' export const useUpdateVersionHook = () => { const { data: localVersionResponse } = useGetLocalVersion() @@ -11,12 +11,12 @@ export const useUpdateVersionHook = () => { major: remoteMajor, minor: remoteMinor, patch: remotePatch, - } = environmentUtil.extractVersions(remoteVersion.data ?? '') + } = extractVersions(remoteVersion.data ?? '') const { major: localMajor, minor: localMinor, patch: localPatch, - } = environmentUtil.extractVersions(localVersion ?? '') + } = extractVersions(localVersion ?? '') const isMajorUpdate = remoteMajor > localMajor const isMinorUpdate = remoteMinor > localMinor diff --git a/packages/app/src/layouts/DashboardLayout.tsx b/packages/app/src/layouts/DashboardLayout.tsx index bce9a815..bd01c165 100644 --- a/packages/app/src/layouts/DashboardLayout.tsx +++ b/packages/app/src/layouts/DashboardLayout.tsx @@ -9,7 +9,7 @@ interface DashboardLayoutProps { children?: React.ReactNode } -function DashboardLayout({ children }: DashboardLayoutProps) { +const DashboardLayout = ({ children }: DashboardLayoutProps) => { const { isMajorUpdate, isMinorUpdate } = useUpdateVersionHook() if (isMajorUpdate || isMinorUpdate) { return @@ -25,4 +25,4 @@ function DashboardLayout({ children }: DashboardLayoutProps) { ) } -export default DashboardLayout +export { DashboardLayout } diff --git a/packages/app/src/layouts/ExtensionsLayout.tsx b/packages/app/src/layouts/ExtensionsLayout.tsx index c9f3b528..343e7285 100644 --- a/packages/app/src/layouts/ExtensionsLayout.tsx +++ b/packages/app/src/layouts/ExtensionsLayout.tsx @@ -1,9 +1,9 @@ import { Box, Typography } from '@mui/material' import { Outlet, useLocation, useNavigate } from 'react-router-dom' -import CodeClimbersButton from '../components/common/CodeClimbersButton' import { Logo } from '../components/common/Logo/Logo' -import extensionsService from '../services/extensions.service' +import { getExtensionByRoute } from '../services/extensions.service' +import { CodeClimbersButton } from '../components/common/CodeClimbersButton' interface Props { children?: React.ReactNode @@ -12,9 +12,7 @@ interface Props { export const ExtensionsLayout = ({ children }: Props) => { const navigate = useNavigate() const location = useLocation() - const currentExtension = extensionsService.getExtensionByRoute( - location.pathname, - ) + const currentExtension = getExtensionByRoute(location.pathname) const title = currentExtension?.name const handleClick = () => { navigate('/') diff --git a/packages/app/src/layouts/ImportLayout.tsx b/packages/app/src/layouts/ImportLayout.tsx index 6c84e1c6..06531136 100644 --- a/packages/app/src/layouts/ImportLayout.tsx +++ b/packages/app/src/layouts/ImportLayout.tsx @@ -5,8 +5,8 @@ interface ImportLayoutProps { children?: React.ReactNode } -function ImportLayout({ children }: ImportLayoutProps) { +const ImportLayout = ({ children }: ImportLayoutProps) => { return {children || } } -export default ImportLayout +export { ImportLayout } diff --git a/packages/app/src/layouts/InstallLayout.tsx b/packages/app/src/layouts/InstallLayout.tsx index f9c47690..2e333c80 100644 --- a/packages/app/src/layouts/InstallLayout.tsx +++ b/packages/app/src/layouts/InstallLayout.tsx @@ -4,8 +4,8 @@ interface BaseLayoutProps { children?: React.ReactNode } -function BaseLayout({ children }: BaseLayoutProps) { +const BaseLayout = ({ children }: BaseLayoutProps) => { return <>{children || } } -export default BaseLayout +export { BaseLayout } diff --git a/packages/app/src/providers/localStorageAuthProvider.tsx b/packages/app/src/providers/localStorageAuthProvider.tsx index a048c325..2d43e444 100644 --- a/packages/app/src/providers/localStorageAuthProvider.tsx +++ b/packages/app/src/providers/localStorageAuthProvider.tsx @@ -1,20 +1,20 @@ import { useGetLocalApiKey } from '../services/localAuth.service' import { LoadingScreen } from '../components/LoadingScreen' -import authUtil from '../utils/auth.util' +import { getLocalApiKey, setLocalApiKey } from '../utils/auth.util' interface Props { children: React.ReactNode } export const LocalStorageAuthProvider = ({ children }: Props) => { - const localApiKey = authUtil.getLocalApiKey() + const localApiKey = getLocalApiKey() const { data, isFetching } = useGetLocalApiKey(!localApiKey) if (isFetching) { return } if (data?.apiKey) { - authUtil.setLocalApiKey(data.apiKey) + setLocalApiKey(data.apiKey) } return <>{children} } diff --git a/packages/app/src/repos/pulse.repo.ts b/packages/app/src/repos/pulse.repo.ts index 81766d12..5b45fa13 100644 --- a/packages/app/src/repos/pulse.repo.ts +++ b/packages/app/src/repos/pulse.repo.ts @@ -1,30 +1,28 @@ -import dbUtil from '../utils/db.util' +import { deepWorkSql } from './queries/deepWork.query' const getLatestPulses = () => { // Example query - const query = dbUtil.db - .selectFrom('activities_pulse') - .selectAll() - .orderBy('id', 'desc') - .limit(10) - - // Get the SQL string - const sql = query.compile() - - return dbUtil.sqlWithBindings(sql) + const query = ` + SELECT * + FROM activities_pulse + ORDER BY id DESC + LIMIT 10 + ` + return query } const getAllPulses = () => { - const query = dbUtil.db - .selectFrom('activities_pulse') - .selectAll() - .orderBy('created_at', 'desc') - const sql = query.compile() - - return dbUtil.sqlWithBindings(sql) + const query = ` + SELECT * + FROM activities_pulse + ORDER BY created_at DESC + ` + return query } -export default { - getAllPulses, - getLatestPulses, +const getDeepWork = (startDate: string, endDate: string) => { + const query = deepWorkSql(startDate, endDate) + return query } + +export { getAllPulses, getLatestPulses, getDeepWork } diff --git a/packages/app/src/repos/queries/deepWork.query.ts b/packages/app/src/repos/queries/deepWork.query.ts new file mode 100644 index 00000000..87288961 --- /dev/null +++ b/packages/app/src/repos/queries/deepWork.query.ts @@ -0,0 +1,36 @@ +export const deepWorkSql = (startDate: string, endDate: string) => ` + WITH get_periods AS ( + select MIN(time) AS interval_start, + COUNT(*) AS activity_count, + (strftime('%s', time) / 120) AS interval_id + from activities_pulse + where time BETWEEN '${startDate}' AND '${endDate}' + group by (strftime('%s', time) / 120) + order by interval_id asc +), + + flagged AS ( + SELECT *, + (strftime('%s', interval_start) - strftime('%s', LAG(interval_start) OVER (ORDER BY interval_start)) <= 240) AS within_4_minutes + FROM get_periods + ), + groups AS ( + SELECT *, + SUM(CASE WHEN within_4_minutes = 0 THEN 1 ELSE 0 END) OVER (ORDER BY interval_start) AS reset_group + FROM flagged + ), + flow_states AS ( + SELECT *, + SUM(within_4_minutes) OVER (PARTITION BY reset_group ORDER BY interval_start) AS cumulative_within_4_minutes + FROM groups + ), + flow_final AS ( + + select reset_group as flow_group, min(interval_start) as flow_start, (max(cumulative_within_4_minutes) + 1) * 2 as flow_time + from flow_states + group by flow_group + ) + +select * from flow_final +where flow_time > 14; +` diff --git a/packages/app/src/repos/user.repo.ts b/packages/app/src/repos/user.repo.ts new file mode 100644 index 00000000..8fe47ecd --- /dev/null +++ b/packages/app/src/repos/user.repo.ts @@ -0,0 +1,39 @@ +import { db, sqlWithBindings } from '../utils/db.util' + +const getCurrentUser = () => { + // Example query + const query = ` + SELECT * + FROM accounts_user + JOIN accounts_user_settings ON accounts_user.id = accounts_user_settings.user_id + LIMIT 1 + ` + return query +} + +const updateUser = (userId: number, user: Partial) => { + const query = db + .updateTable('accounts_user') + .set(user) + .where('id', '=', userId) + .returningAll() + + const sql = sqlWithBindings(query.compile()) + return sql +} + +const updateUserSettings = ( + userId: number, + settings: Partial, +) => { + const query = db + .updateTable('accounts_user_settings') + .set(settings) + .where('user_id', '=', userId) + .returningAll() + + const sql = sqlWithBindings(query.compile()) + return sql +} + +export { getCurrentUser, updateUser, updateUserSettings } diff --git a/packages/app/src/routes/AppRoutes.tsx b/packages/app/src/routes/AppRoutes.tsx index b93b932b..f5d627df 100644 --- a/packages/app/src/routes/AppRoutes.tsx +++ b/packages/app/src/routes/AppRoutes.tsx @@ -1,17 +1,17 @@ import { Route, Routes } from 'react-router-dom' -import InstallPage from '../components/InstallPage' -import ImportLayout from '../layouts/ImportLayout' import { ImportPage } from '../components/ImportPage' -import DashboardLayout from '../layouts/DashboardLayout' import { ExtensionsPage } from '../components/Extensions/ExtensionsPage' -import extensionsService from '../services/extensions.service' import { ExtensionsLayout } from '../layouts/ExtensionsLayout' import { ContributorsPage } from '../components/ContributorsPage' -import HomePage from '../components/Home/HomePage' +import { getActiveDashboardExtensionRoutes } from '../services/extensions.service' +import { HomePage } from '../components/Home/HomePage' +import { InstallPage } from '../components/InstallPage' +import { DashboardLayout } from '../layouts/DashboardLayout' +import { ImportLayout } from '../layouts/ImportLayout' export const AppRoutes = () => { - const extensions = extensionsService.getActiveDashboardExtensionRoutes() + const extensions = getActiveDashboardExtensionRoutes() return ( <> diff --git a/packages/app/src/routes/index.tsx b/packages/app/src/routes/index.tsx index c80b4c22..345d9f88 100644 --- a/packages/app/src/routes/index.tsx +++ b/packages/app/src/routes/index.tsx @@ -2,7 +2,7 @@ import { BrowserRouter } from 'react-router-dom' import { AppRoutes } from './AppRoutes' import { useVersionConsoleBanner } from '../hooks/useVersionConsole' -function AppRouter() { +const AppRouter = () => { useVersionConsoleBanner() return ( <> @@ -13,4 +13,4 @@ function AppRouter() { ) } -export default AppRouter +export { AppRouter } diff --git a/packages/app/src/services/contributors.service.ts b/packages/app/src/services/contributors.service.ts index 33bcaf8a..c66db229 100644 --- a/packages/app/src/services/contributors.service.ts +++ b/packages/app/src/services/contributors.service.ts @@ -102,4 +102,4 @@ const getSpotlight = (): Contributor => { ] } -export default { getSpotlight, getContributors } +export { getSpotlight, getContributors } diff --git a/packages/app/src/services/extensions.service.ts b/packages/app/src/services/extensions.service.ts index 91bbbd72..112aac6c 100644 --- a/packages/app/src/services/extensions.service.ts +++ b/packages/app/src/services/extensions.service.ts @@ -4,9 +4,9 @@ */ // eslint-disable-next-line import/no-named-as-default import posthog from 'posthog-js' -import SqlSandbox from '../extensions/SqlSandbox' -import SqlSandboxPage from '../extensions/SqlSandbox/SqlSandboxPage' -import sqlSandboxService from '../extensions/SqlSandbox/sqlSandbox.service' +import { SqlSandbox } from '../extensions/SqlSandbox' +import { SqlSandboxPage } from '../extensions/SqlSandbox/SqlSandboxPage' +import { onAdd } from '../extensions/SqlSandbox/sqlSandbox.service' const EXTENSIONS_KEY = 'activated-extensions' @@ -45,7 +45,7 @@ const extensions: (Extension | DashboardExtension)[] = [ route: '/sql-sandbox', pageComponent: SqlSandboxPage, onAdd: () => { - sqlSandboxService.onAdd() + onAdd() }, createdAt: new Date('2024-09-16'), isPopular: true, @@ -62,7 +62,7 @@ const extensions: (Extension | DashboardExtension)[] = [ }, ] -function getActiveExtensionIds(): string[] { +const getActiveExtensionIds = (): string[] => { const rawExtensions = localStorage.getItem(EXTENSIONS_KEY) const extensionIds = rawExtensions ? (JSON.parse(rawExtensions) as string[]) @@ -70,7 +70,7 @@ function getActiveExtensionIds(): string[] { return extensionIds } -function getActiveExtensions(): Extension[] { +const getActiveExtensions = (): Extension[] => { const extensionIds = getActiveExtensionIds() const activeExtensions = extensionIds.map((id) => extensions.find((extension) => extension.id === id), @@ -78,29 +78,29 @@ function getActiveExtensions(): Extension[] { return activeExtensions.filter((extension) => extension !== undefined) } -function getActiveDashboardExtensions(): DashboardExtension[] { +const getActiveDashboardExtensions = (): DashboardExtension[] => { const activeExtensions = getActiveExtensions() return activeExtensions.filter( (extension): extension is DashboardExtension => 'component' in extension, ) } -function getActiveDashboardExtensionRoutes(): DashboardExtension[] { +const getActiveDashboardExtensionRoutes = (): DashboardExtension[] => { const activeExtensions = getActiveDashboardExtensions() return activeExtensions.filter((extension) => extension.route) } -function getExtensionByRoute(route: string): DashboardExtension | undefined { +const getExtensionByRoute = (route: string): DashboardExtension | undefined => { return getActiveDashboardExtensions().find( (extension) => extension.route === route, ) } -function isExtensionAdded(extensionId: string) { +const isExtensionAdded = (extensionId: string): boolean => { return getActiveExtensionIds().includes(extensionId) } -function addExtension(extensionId: string) { +const addExtension = (extensionId: string): void => { if (!extensionId) return const extensions = getActiveExtensionIds() if (isExtensionAdded(extensionId)) { @@ -116,7 +116,7 @@ function addExtension(extensionId: string) { }) } -function removeExtension(extensionId: string) { +const removeExtension = (extensionId: string): void => { if (!extensionId) return const extensionIds = getActiveExtensionIds() const newExtensions = extensionIds.filter((id) => id !== extensionId) @@ -129,11 +129,11 @@ function removeExtension(extensionId: string) { }) } -function getPopularExtension(): Extension | undefined { +const getPopularExtension = (): Extension | undefined => { return extensions.find((extension) => extension.isPopular) } -function getNewestExtension(): Extension | undefined { +const getNewestExtension = (): Extension | undefined => { return extensions.reduce( (newest, extension) => { if (!newest) return extension @@ -144,8 +144,12 @@ function getNewestExtension(): Extension | undefined { ) } -export default { - extensions, +const getExtensions = (): Extension[] => { + return extensions +} + +export { + getExtensions, getActiveExtensions, addExtension, removeExtension, diff --git a/packages/app/src/services/health.service.ts b/packages/app/src/services/health.service.ts index 42a129ec..6d8054f0 100644 --- a/packages/app/src/services/health.service.ts +++ b/packages/app/src/services/health.service.ts @@ -1,7 +1,7 @@ import { BASE_API_URL, useBetterQuery } from '.' import { apiRequest } from '../utils/request' -export function useGetHealth( +export const useGetHealth = ( { refetchInterval = 1000, retry = false, @@ -10,7 +10,7 @@ export function useGetHealth( retry?: boolean } = {}, page: 'home' | 'install' = 'home', -) { +) => { const queryFn = () => apiRequest({ url: `${BASE_API_URL}/health`, @@ -30,7 +30,7 @@ export function useGetHealth( }) } -export function useGetLocalVersion() { +export const useGetLocalVersion = () => { const queryFn = () => apiRequest({ url: `${BASE_API_URL}/health`, diff --git a/packages/app/src/services/keys.ts b/packages/app/src/services/keys.ts index f5cc2904..96e71243 100644 --- a/packages/app/src/services/keys.ts +++ b/packages/app/src/services/keys.ts @@ -14,3 +14,7 @@ export const pulseKeys = { perProjectOverviewTopThree: (startDate: string, endDate: string) => ['perProjectTimeOverview', 'topThree', startDate, endDate] as const, } + +export const userKeys = { + user: ['user'] as const, +} diff --git a/packages/app/src/services/localAuth.service.ts b/packages/app/src/services/localAuth.service.ts index 8e823fd4..b6e872dc 100644 --- a/packages/app/src/services/localAuth.service.ts +++ b/packages/app/src/services/localAuth.service.ts @@ -1,7 +1,7 @@ import { BASE_API_URL, useBetterQuery } from '.' import { apiRequest } from '../utils/request' -export function useGetLocalApiKey(enabled = true) { +export const useGetLocalApiKey = (enabled = true) => { const queryFn = () => apiRequest({ url: `${BASE_API_URL}/auth/local`, @@ -15,9 +15,9 @@ export function useGetLocalApiKey(enabled = true) { }) } -export function useValidateLocalApiKey( +export const useValidateLocalApiKey = ( page: 'import' | 'home' | 'banner' = 'home', -) { +) => { const queryFn = () => apiRequest({ url: `${BASE_API_URL}/auth/local/validate`, diff --git a/packages/app/src/services/pulse.service.ts b/packages/app/src/services/pulse.service.ts index 141c8f16..5b24e371 100644 --- a/packages/app/src/services/pulse.service.ts +++ b/packages/app/src/services/pulse.service.ts @@ -3,23 +3,9 @@ import { BASE_API_URL, useBetterQuery } from '.' import { apiRequest } from '../utils/request' import { pulseKeys } from './keys' import { Dayjs } from 'dayjs' -import pulseRepo from '../repos/pulse.repo' -import queryApi from './query.service' -import { getFeatureFlag } from '../utils/flag.util' -import csvUtil from '../utils/csv.util' +import { downloadBlob } from '../utils/csv.util' -export function useLatestPulses() { - const queryFn = () => { - const sql = pulseRepo.getLatestPulses() - return queryApi.sqlQueryFn(sql) - } - return useBetterQuery({ - queryKey: pulseKeys.latestPulses, - queryFn, - }) -} - -export function useGetSources() { +const useGetSources = () => { const queryFn = () => apiRequest({ url: `${BASE_API_URL}/pulses/sources`, @@ -32,7 +18,7 @@ export function useGetSources() { }) } -export function useGetSourcesWithMinutes(startDate: string, endDate: string) { +const useGetSourcesWithMinutes = (startDate: string, endDate: string) => { const queryFn = () => apiRequest({ url: `${BASE_API_URL}/pulses/sourcesMinutes?startDate=${startDate}&endDate=${endDate}`, @@ -45,7 +31,7 @@ export function useGetSourcesWithMinutes(startDate: string, endDate: string) { }) } -export function useGetSitesWithMinutes(startDate: string, endDate: string) { +const useGetSitesWithMinutes = (startDate: string, endDate: string) => { const queryFn = () => apiRequest({ url: `${BASE_API_URL}/pulses/sitesMinutes?startDate=${startDate}&endDate=${endDate}`, @@ -58,23 +44,16 @@ export function useGetSitesWithMinutes(startDate: string, endDate: string) { }) } -export function useExportPulses() { +const useExportPulses = () => { const exportPulses = useCallback(async () => { try { - let blob - if (getFeatureFlag('DirectQueryAPI')) { - const response = await queryApi.sqlQueryFn(pulseRepo.getAllPulses()) - const csvContent = csvUtil.convertRecordsToCSV(response) - blob = new Blob([csvContent]) - } else { - const response = await apiRequest({ - url: `${BASE_API_URL}/pulses/export`, - method: 'GET', - responseType: 'blob', - }) - blob = new Blob([response]) - } - csvUtil.downloadBlob(blob, 'pulses.csv') + const response = await apiRequest({ + url: `${BASE_API_URL}/pulses/export`, + method: 'GET', + responseType: 'blob', + }) + const blob = new Blob([response]) + downloadBlob(blob, 'pulses.csv') } catch (error) { console.error('Failed to export pulses:', error) } @@ -83,7 +62,7 @@ export function useExportPulses() { return { exportPulses } } -export function useWeekOverview(date = '') { +const useWeekOverview = (date = '') => { const queryFn = () => apiRequest({ url: `${BASE_API_URL}/pulses/weekOverview?date=${date}`, @@ -96,7 +75,7 @@ export function useWeekOverview(date = '') { }) } -export function useCategoryTimeOverview(selectedStartDate: Dayjs) { +const useCategoryTimeOverview = (selectedStartDate: Dayjs) => { const todayStartDate = selectedStartDate?.startOf('day').toISOString() const todayEndDate = selectedStartDate?.endOf('day').toISOString() @@ -125,7 +104,7 @@ export function useCategoryTimeOverview(selectedStartDate: Dayjs) { }) } -export function useDeepWork(selectedStartDate: Dayjs) { +const useDeepWork = (selectedStartDate: Dayjs) => { const startDate = selectedStartDate?.startOf('day').toISOString() const endDate = selectedStartDate?.endOf('day').toISOString() const queryFn = () => @@ -140,7 +119,7 @@ export function useDeepWork(selectedStartDate: Dayjs) { }) } -export function usePerProjectOverviewTopThree(selectedStartDate: Dayjs) { +const usePerProjectOverviewTopThree = (selectedStartDate: Dayjs) => { const startDate = selectedStartDate?.startOf('day').toISOString() const endDate = selectedStartDate?.endOf('day').toISOString() const queryFn = () => @@ -154,3 +133,14 @@ export function usePerProjectOverviewTopThree(selectedStartDate: Dayjs) { enabled: !!startDate && !!endDate, }) } + +export { + useGetSources, + useGetSourcesWithMinutes, + useGetSitesWithMinutes, + useExportPulses, + useWeekOverview, + useCategoryTimeOverview, + useDeepWork, + usePerProjectOverviewTopThree, +} diff --git a/packages/app/src/services/version.service.ts b/packages/app/src/services/version.service.ts index f20b5e75..4d29be29 100644 --- a/packages/app/src/services/version.service.ts +++ b/packages/app/src/services/version.service.ts @@ -1,10 +1,10 @@ import { useBetterQuery } from '.' -import environmentUtil from '../utils/environment.util' +import { getFEEnvironment } from '../utils/environment.util' const THREE_MINUTES = 3 * 60 * 1_000 -export function useLatestVersion(enabled = true) { - const environment = environmentUtil.getFEEnvironment() +export const useLatestVersion = (enabled = true) => { + const environment = getFEEnvironment() const packageJsonUrl = environment === 'release' diff --git a/packages/app/src/utils/auth.util.ts b/packages/app/src/utils/auth.util.ts index 18d6b004..43c38e0b 100644 --- a/packages/app/src/utils/auth.util.ts +++ b/packages/app/src/utils/auth.util.ts @@ -1,6 +1,6 @@ // function to get api key from local storage const LOCAL_API_KEY = 'local_api_key' -function getLocalApiKey() { +const getLocalApiKey = (): string | null => { const apiKey = localStorage.getItem(LOCAL_API_KEY) if (apiKey === 'undefined') { return null @@ -8,9 +8,9 @@ function getLocalApiKey() { return apiKey } -function setLocalApiKey(apiKey: string) { +const setLocalApiKey = (apiKey: string) => { if (apiKey === 'undefined') return localStorage.setItem(LOCAL_API_KEY, apiKey) } -export default { getLocalApiKey, setLocalApiKey } +export { getLocalApiKey, setLocalApiKey } diff --git a/packages/app/src/utils/csv.util.ts b/packages/app/src/utils/csv.util.ts index 9687a2ec..840b21d2 100644 --- a/packages/app/src/utils/csv.util.ts +++ b/packages/app/src/utils/csv.util.ts @@ -20,7 +20,4 @@ const downloadBlob = (blob: Blob, filename = 'data.csv') => { window.URL.revokeObjectURL(encodedUri) } -export default { - convertRecordsToCSV, - downloadBlob, -} +export { convertRecordsToCSV, downloadBlob } diff --git a/packages/app/src/utils/db.util.ts b/packages/app/src/utils/db.util.ts index 1caaef07..924f60c3 100644 --- a/packages/app/src/utils/db.util.ts +++ b/packages/app/src/utils/db.util.ts @@ -7,11 +7,10 @@ import { CompiledQuery, } from 'kysely' -interface Database { - activities_pulse: CodeClimbers.PulseDB -} +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Database = Record -const db = new Kysely({ +export const db = new Kysely({ dialect: { createAdapter: () => new PostgresAdapter(), createDriver: () => new DummyDriver(), @@ -28,7 +27,7 @@ declare module 'kysely' { } // Implement the sqlWithBindings method -function sqlWithBindings(compiledQuery: CompiledQuery): string { +export const sqlWithBindings = (compiledQuery: CompiledQuery): string => { let sql = compiledQuery.sql const parameters = compiledQuery.parameters @@ -54,8 +53,3 @@ function sqlWithBindings(compiledQuery: CompiledQuery): string { return sql } - -export default { - db, - sqlWithBindings, -} diff --git a/packages/app/src/utils/environment.util.ts b/packages/app/src/utils/environment.util.ts index ae46fe69..2a0f02ce 100644 --- a/packages/app/src/utils/environment.util.ts +++ b/packages/app/src/utils/environment.util.ts @@ -22,7 +22,7 @@ const getFEEnvironment = (): FEEnvironment => { return 'unknown' } -export default { +export { isBrowserCli, extractVersions, isReleaseSite, diff --git a/packages/app/src/utils/request.ts b/packages/app/src/utils/request.ts index e7f5a13d..8aad1d17 100644 --- a/packages/app/src/utils/request.ts +++ b/packages/app/src/utils/request.ts @@ -1,7 +1,7 @@ -import authUtil from './auth.util' -import environmentUtil from './environment.util' +import { getLocalApiKey } from './auth.util' +import { isBrowserCli } from './environment.util' -const BASE_URL = environmentUtil.isBrowserCli ? '' : 'http://localhost:14400' +const BASE_URL = isBrowserCli ? '' : 'http://localhost:14400' export class ApiError extends Error { statusCode: number @@ -12,9 +12,9 @@ export class ApiError extends Error { } } -export function getUrlParameters( +export const getUrlParameters = ( data: Record, -) { +) => { const ret = [] for (const d in data) { const param = data[d] @@ -25,7 +25,7 @@ export function getUrlParameters( return ret.join('&') } -export async function apiRequest({ +export const apiRequest = async ({ url, method = 'GET', body, @@ -39,8 +39,8 @@ export async function apiRequest({ responseType?: 'json' | 'text' | 'blob' | 'arraybuffer' headers?: Record credentials?: RequestCredentials -}) { - const apiKey = authUtil.getLocalApiKey() +}) => { + const apiKey = getLocalApiKey() if (apiKey) { headers = { ...headers, diff --git a/packages/app/src/utils/time.ts b/packages/app/src/utils/time.ts index 47660a7f..9c1ccec8 100644 --- a/packages/app/src/utils/time.ts +++ b/packages/app/src/utils/time.ts @@ -24,6 +24,6 @@ dayjs.updateLocale('en', { }, }) -export function getTimeSince(utcDateString: string): string { +export const getTimeSince = (utcDateString: string): string => { return dayjs(utcDateString).fromNow(true) } diff --git a/packages/server/commands/start/index.ts b/packages/server/commands/start/index.ts index 1bbe6fbe..d94646f9 100644 --- a/packages/server/commands/start/index.ts +++ b/packages/server/commands/start/index.ts @@ -14,7 +14,7 @@ import { START_ERR_LOG_MESSAGE } from '../../utils/node.util' const MAX_ATTEMPTS = 10 const POLL_INTERVAL = 3000 // 3 seconds -function checkServerAvailability(url: string): Promise { +const checkServerAvailability = (url: string): Promise => { return new Promise((resolve) => { http .get(url, (res) => { diff --git a/packages/server/src/main.ts b/packages/server/src/main.ts index 9b53c4fb..9b487607 100644 --- a/packages/server/src/main.ts +++ b/packages/server/src/main.ts @@ -20,7 +20,7 @@ const traceEnvironment = () => { Logger.debug(`process.env: ${JSON.stringify(process.env)}`, 'main.ts') } -export async function bootstrap() { +export const bootstrap = async () => { const port = process.env.CODECLIMBERS_SERVER_PORT || 14_400 const app = await NestFactory.create(AppModule, { logger: !isProd() diff --git a/packages/server/src/v1/activities/activities.service.ts b/packages/server/src/v1/activities/activities.service.ts index cf5f6376..fc3f0dfc 100644 --- a/packages/server/src/v1/activities/activities.service.ts +++ b/packages/server/src/v1/activities/activities.service.ts @@ -1,11 +1,18 @@ import { Injectable } from '@nestjs/common' import { CreateWakatimePulseDto } from '../dtos/createWakatimePulse.dto' -import activitiesUtil from '../../../utils/activities.util' import { PulseRepo } from '../database/pulse.repo' import os from 'node:os' import dayjs from 'dayjs' import { TimePeriodDto } from '../dtos/getCategoryTimeOverview.dto' import { PageDto, PageMetaDto, PageOptionsDto } from '../dtos/pagination.dto' +import { + mapStatusBarRawToDto, + pulseSuccessResponse, + filterUniqueByHash, + getSourceFromUserAgent, + calculatePulseHash, +} from '../../../utils/activities.util' +import { assert } from 'node:console' @Injectable() export class ActivitiesService { @@ -22,7 +29,7 @@ export class ActivitiesService { endDate, ) const dayTotalMinutes = data.reduce((acc, curr) => acc + curr.minutes, 0) - return activitiesUtil.mapStatusBarRawToDto(statusBarRaw, dayTotalMinutes) + return mapStatusBarRawToDto(statusBarRaw, dayTotalMinutes) } // process the pulse async createPulse(pulseDto: CreateWakatimePulseDto) { @@ -32,18 +39,21 @@ export class ActivitiesService { latestProject, ) await this.pulseRepo.createPulse(pulse) - return activitiesUtil.pulseSuccessResponse(1) + return pulseSuccessResponse(1) } async createPulses(pulsesDto: CreateWakatimePulseDto[]) { + assert(!pulsesDto, 'pulsesDto required') + assert(!Array.isArray(pulsesDto), 'Pulses must be an array') + const latestProject = await this.pulseRepo.getLatestProject() const pulses: CodeClimbers.Pulse[] = pulsesDto.map((dto) => this.mapDtoToPulse(dto, latestProject), ) - const uniquePulses = activitiesUtil.filterUniqueByHash(pulses) + const uniquePulses = filterUniqueByHash(pulses) await this.pulseRepo.createPulses(uniquePulses) - return activitiesUtil.pulseSuccessResponse(pulsesDto.length) + return pulseSuccessResponse(pulsesDto.length) } async getLatestPulses(): Promise { @@ -109,7 +119,7 @@ export class ActivitiesService { const sources = new Set() userAgentsAndLastActive.forEach((userAgent) => { - const source = activitiesUtil.getSourceFromUserAgent(userAgent.userAgent) + const source = getSourceFromUserAgent(userAgent.userAgent) if (source) { sources.add(source) } @@ -118,10 +128,7 @@ export class ActivitiesService { return Array.from(sources).map((source) => { const maxLastActive = userAgentsAndLastActive .filter((userAgent) => { - return ( - source === - activitiesUtil.getSourceFromUserAgent(userAgent.userAgent) - ) + return source === getSourceFromUserAgent(userAgent.userAgent) }) .reduce((max, userAgent) => { return new Date(userAgent.lastActive) > new Date(max) @@ -224,7 +231,7 @@ export class ActivitiesService { machine: dto.machine || os.hostname(), userAgent: dto.user_agent || this.userAgent(), time: dayjs((dto.time as number) * 1000).toISOString(), - hash: `${activitiesUtil.calculatePulseHash(dto)}`, + hash: `${calculatePulseHash(dto)}`, origin: dto.origin || '', originId: dto.origin_id || '', category: dto.category || '', diff --git a/packages/server/src/v1/database/knex.ts b/packages/server/src/v1/database/knex.ts index 597cb0d1..f0be264e 100644 --- a/packages/server/src/v1/database/knex.ts +++ b/packages/server/src/v1/database/knex.ts @@ -15,9 +15,9 @@ import { initDBDir, DB_PATH, BIN_PATH } from '../../../utils/node.util' const deepMapKeys = function (obj: any, fn: any) { const x: { [key: string]: any } = {} - forOwn(obj, function (v, k) { + forOwn(obj, (v, k) => { if (Array.isArray(v)) { - v = v.map(function (x) { + v = v.map((x) => { return isPlainObject(x) ? deepMapKeys(x, fn) : x }) } diff --git a/packages/server/src/v1/database/models/user.d.ts b/packages/server/src/v1/database/models/user.d.ts new file mode 100644 index 00000000..34693113 --- /dev/null +++ b/packages/server/src/v1/database/models/user.d.ts @@ -0,0 +1,21 @@ +declare namespace CodeClimbers { + export interface User { + id?: number + email: string + firstName?: string + lastName?: string + avatarUrl?: string + createdAt: string + updatedAt: string + } + // same as User but snake case + export interface UserDB { + id?: number + email: string + first_name?: string + last_name?: string + avatar_url?: string + created_at: string + updated_at: string + } +} diff --git a/packages/server/src/v1/database/models/user_setting.d.ts b/packages/server/src/v1/database/models/user_setting.d.ts new file mode 100644 index 00000000..b0c15010 --- /dev/null +++ b/packages/server/src/v1/database/models/user_setting.d.ts @@ -0,0 +1,18 @@ +declare namespace CodeClimbers { + export type WeeklyReportType = 'ai' | 'standard' | 'none' | '' + export interface UserSettings { + id?: number + userId: number + weeklyReportType: WeeklyReportType + createdAt: string + updatedAt: string + } + // same as User but snake case + export interface UserSettingsDB { + id?: number + user_id: number + weekly_report_type: WeeklyReportType + created_at: string + updated_at: string + } +} diff --git a/packages/server/src/v1/database/pulse.repo.ts b/packages/server/src/v1/database/pulse.repo.ts index 8b9c8dbe..2c623e79 100644 --- a/packages/server/src/v1/database/pulse.repo.ts +++ b/packages/server/src/v1/database/pulse.repo.ts @@ -1,8 +1,8 @@ import { Injectable, Logger } from '@nestjs/common' import { InjectKnex, Knex } from 'nestjs-knex' -import sqlReaderUtil from '../../../utils/sqlReader.util' import dayjs from 'dayjs' import { PageOptionsDto } from '../dtos/pagination.dto' +import { getFileContentAsString } from '../../../utils/sqlReader.util' interface MinutesQuery { minutes: number @@ -17,9 +17,7 @@ export class PulseRepo { async getStatusBarDetails(): Promise { const startOfDay = dayjs().startOf('day').toISOString() const endOfDay = dayjs().endOf('day').toISOString() - const getTimeQuery = await sqlReaderUtil.getFileContentAsString( - 'getStatusBarDetails.sql', - ) + const getTimeQuery = await getFileContentAsString('getStatusBarDetails.sql') return this.knex.raw(getTimeQuery, { startOfDay, endOfDay }) } @@ -50,10 +48,9 @@ export class PulseRepo { startDate: Date, endDate: Date, ): Promise { - const getLongestDayMinutesQuery = - await sqlReaderUtil.getFileContentAsString( - 'getLongestDayInRangeMinutes.sql', - ) + const getLongestDayMinutesQuery = await getFileContentAsString( + 'getLongestDayInRangeMinutes.sql', + ) const [result] = await this.knex.raw( getLongestDayMinutesQuery, { @@ -129,7 +126,7 @@ export class PulseRepo { endDate: string, ): Promise { const getLongestDayMinutesQuery = - await sqlReaderUtil.getFileContentAsString('getDeepWork.sql') + await getFileContentAsString('getDeepWork.sql') const result = await this.knex.raw( getLongestDayMinutesQuery, { diff --git a/packages/server/src/v1/startup/darwinStartup.service.ts b/packages/server/src/v1/startup/darwinStartup.service.ts index 50ae773b..3302543f 100644 --- a/packages/server/src/v1/startup/darwinStartup.service.ts +++ b/packages/server/src/v1/startup/darwinStartup.service.ts @@ -5,9 +5,9 @@ import { CODE_CLIMBER_META_DIR, NODE_PATH, } from '../../../utils/node.util' -import startupUtil from './startup.util' +import { getServiceLib } from './startup.util' import { isDev } from '../../../utils/environment.util' -const { Service } = startupUtil.getServiceLib() +const { Service } = getServiceLib() @Injectable() export class DarwinStartupService implements CodeClimbers.StartupService { diff --git a/packages/server/src/v1/startup/linuxStartup.service.ts b/packages/server/src/v1/startup/linuxStartup.service.ts index cd637bfb..40287623 100644 --- a/packages/server/src/v1/startup/linuxStartup.service.ts +++ b/packages/server/src/v1/startup/linuxStartup.service.ts @@ -5,9 +5,9 @@ import { CODE_CLIMBER_META_DIR, NODE_PATH, } from '../../../utils/node.util' -import startupUtil from './startup.util' +import { getServiceLib } from './startup.util' import { isDev } from '../../../utils/environment.util' -const { Service } = startupUtil.getServiceLib() +const { Service } = getServiceLib() @Injectable() export class LinuxStartupService implements CodeClimbers.StartupService { diff --git a/packages/server/src/v1/startup/startup.util.ts b/packages/server/src/v1/startup/startup.util.ts index 407cab56..229dd9a0 100644 --- a/packages/server/src/v1/startup/startup.util.ts +++ b/packages/server/src/v1/startup/startup.util.ts @@ -2,7 +2,7 @@ * these modules are not available on all platforms, so we have to import them conditionally * or we'll get build and runtime errors */ -function getServiceLib() { +const getServiceLib = () => { const os = process.platform switch (os) { case 'darwin': @@ -16,6 +16,4 @@ function getServiceLib() { } } -export default { - getServiceLib, -} +export { getServiceLib } diff --git a/packages/server/src/v1/startup/windowsStartup.service.ts b/packages/server/src/v1/startup/windowsStartup.service.ts index 24d9c79f..cf0558a1 100644 --- a/packages/server/src/v1/startup/windowsStartup.service.ts +++ b/packages/server/src/v1/startup/windowsStartup.service.ts @@ -2,9 +2,9 @@ import { Injectable, Logger } from '@nestjs/common' // eslint-disable-next-line import/no-unresolved import * as path from 'node:path' import { BIN_PATH, CODE_CLIMBER_META_DIR } from '../../../utils/node.util' -import startupUtil from './startup.util' +import { getServiceLib } from './startup.util' import { isDev } from '../../../utils/environment.util' -const { Service } = startupUtil.getServiceLib() +const { Service } = getServiceLib() @Injectable() export class WindowsStartupService implements CodeClimbers.StartupService { diff --git a/packages/server/utils/__tests__/activites.util.test.ts b/packages/server/utils/__tests__/activites.util.test.ts index 3a31fa1f..22687e98 100644 --- a/packages/server/utils/__tests__/activites.util.test.ts +++ b/packages/server/utils/__tests__/activites.util.test.ts @@ -1,4 +1,4 @@ -import activitiesUtil from '../activities.util' +import { getSourceFromUserAgent } from '../activities.util' describe('getSourceFromUserAgent', () => { it(`should return 'vscode' as source of userAgents`, () => { @@ -10,7 +10,7 @@ describe('getSourceFromUserAgent', () => { ] const result = userAgents.map((userAgent) => { - return activitiesUtil.getSourceFromUserAgent(userAgent) + return getSourceFromUserAgent(userAgent) }) expect(result).toEqual(['vscode', 'vscode', 'vscode', 'vscode']) diff --git a/packages/server/utils/activities.util.ts b/packages/server/utils/activities.util.ts index 6d18f142..46f9be70 100644 --- a/packages/server/utils/activities.util.ts +++ b/packages/server/utils/activities.util.ts @@ -16,7 +16,7 @@ const cyrb53 = (str: string, seed = 0) => { return 4294967296 * (2097151 & h2) + (h1 >>> 0) } -const calculatePulseHash = ( +export const calculatePulseHash = ( pulse: CodeClimbers.CreateWakatimePulseDto, ): number => { /* eslint-disable @typescript-eslint/no-unused-vars */ @@ -34,7 +34,9 @@ const calculatePulseHash = ( return hash } -function filterUniqueByHash(arr: CodeClimbers.Pulse[]) { +export const filterUniqueByHash = ( + arr: CodeClimbers.Pulse[], +): CodeClimbers.Pulse[] => { const uniqueMap = new Map() return arr.filter((obj) => { @@ -46,7 +48,7 @@ function filterUniqueByHash(arr: CodeClimbers.Pulse[]) { }) } -function pulseSuccessResponse(n: number) { +export const pulseSuccessResponse = (n: number) => { const responses = [...Array(n).keys()].map((i) => [null, 201]) return { @@ -54,7 +56,7 @@ function pulseSuccessResponse(n: number) { } } -function defaultStatusBar(): CodeClimbers.ActivitiesStatusBar { +export const defaultStatusBar = (): CodeClimbers.ActivitiesStatusBar => { const now = dayjs() return { @@ -86,10 +88,10 @@ function defaultStatusBar(): CodeClimbers.ActivitiesStatusBar { cached_at: now.toISOString(), } } -function getStatusByKey( +export const getStatusByKey = ( data: CodeClimbers.WakatimePulseStatusDao[], dataKey: string, -): CodeClimbers.ActivitiesDetail[] { +): CodeClimbers.ActivitiesDetail[] => { const keyWithoutS = dataKey.replace(/s$/, '') const groupedData = groupBy(data, keyWithoutS) return Object.keys(groupedData).map((key: string) => { @@ -117,10 +119,10 @@ function getStatusByKey( }) } -function mapStatusBarRawToDto( +export const mapStatusBarRawToDto = ( statusBarRaw: CodeClimbers.WakatimePulseStatusDao[], dayTotalMinutes: number, -): CodeClimbers.ActivitiesStatusBar { +): CodeClimbers.ActivitiesStatusBar => { if (statusBarRaw.length <= 0) return defaultStatusBar() const now = new Date() @@ -169,7 +171,9 @@ function mapStatusBarRawToDto( return statusbar } -function getSourceFromUserAgent(userAgent: string): string | undefined { +export const getSourceFromUserAgent = ( + userAgent: string, +): string | undefined => { const sourceRegex = /.*?\/.*?\s([^0-9]*)\// const match = userAgent.match(sourceRegex) if (match) { @@ -178,12 +182,3 @@ function getSourceFromUserAgent(userAgent: string): string | undefined { return undefined } - -export default { - mapStatusBarRawToDto, - calculatePulseHash, - filterUniqueByHash, - pulseSuccessResponse, - cyrb53, - getSourceFromUserAgent, -} diff --git a/packages/server/utils/helpers.util.ts b/packages/server/utils/helpers.util.ts index a41dbab5..41bff6cf 100644 --- a/packages/server/utils/helpers.util.ts +++ b/packages/server/utils/helpers.util.ts @@ -1,7 +1,7 @@ -export function forOwn( +export const forOwn = ( obj: Record, iteratee: (value: T, key: string, obj: Record) => void, -): void { +): void => { if (obj === null || obj === undefined) { return } @@ -14,7 +14,7 @@ export function forOwn( } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function isPlainObject(value: any): boolean { +export const isPlainObject = (value: any): boolean => { if (typeof value !== 'object' || value === null) { return false } @@ -35,7 +35,7 @@ export function isPlainObject(value: any): boolean { ) } -export function snakeCase(str: string): string { +export const snakeCase = (str: string): string => { if (typeof str !== 'string') { return '' } @@ -70,7 +70,7 @@ export function snakeCase(str: string): string { return result.join('_') } -export function camelCase(str: string): string { +export const camelCase = (str: string): string => { if (typeof str !== 'string') { return '' } @@ -94,8 +94,11 @@ export function camelCase(str: string): string { return result.join('') } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function maxBy(arr: T[], iteratee: (item: T) => any): T | undefined { +export const maxBy = ( + arr: T[], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + iteratee: (item: T) => any, +): T | undefined => { if (!arr || arr.length === 0) return undefined return arr.reduce((acc, item) => { const value = iteratee(item) @@ -104,8 +107,11 @@ export function maxBy(arr: T[], iteratee: (item: T) => any): T | undefined { }, arr[0]) } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function minBy(arr: T[], iteratee: (item: T) => any): T | undefined { +export const minBy = ( + arr: T[], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + iteratee: (item: T) => any, +): T | undefined => { if (!arr || arr.length === 0) return undefined return arr.reduce((acc, item) => { const value = iteratee(item) @@ -115,7 +121,7 @@ export function minBy(arr: T[], iteratee: (item: T) => any): T | undefined { } // groupBy function that takes an array and groups it by a key -export function groupBy(arr: T[], key: string): Record { +export const groupBy = (arr: T[], key: string): Record => { return arr.reduce( (acc, item) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/server/utils/ini.util.ts b/packages/server/utils/ini.util.ts index 2495e8f7..fedcf0ed 100644 --- a/packages/server/utils/ini.util.ts +++ b/packages/server/utils/ini.util.ts @@ -8,7 +8,7 @@ type IniSection = Record export type IniConfig = Record // Function to parse INI content -export function parseIni(content: string): IniConfig { +export const parseIni = (content: string): IniConfig => { const result: IniConfig = {} const lines: string[] = content.split('\n') let currentSection = '' @@ -30,7 +30,7 @@ export function parseIni(content: string): IniConfig { } // Function to stringify INI content -export function stringifyIni(data: IniConfig): string { +export const stringifyIni = (data: IniConfig): string => { const entries = Object.entries(data) .map(([section, entries]) => { const sectionContent: string = Object.entries(entries) @@ -42,27 +42,27 @@ export function stringifyIni(data: IniConfig): string { return `${entries}\n` } -export async function createIniFile( +export const createIniFile = async ( filePath: string, settings: IniConfig, -): Promise { +): Promise => { const iniContent = stringifyIni(settings) await fs.writeFile(filePath, iniContent, 'utf8') } -export async function removeIniFile(filePath: string): Promise { +export const removeIniFile = async (filePath: string): Promise => { await fs.unlink(filePath) } -export async function readIniFile(filePath: string): Promise { +export const readIniFile = async (filePath: string): Promise => { const data = await fs.readFile(filePath, 'utf8') return parseIni(data) } -export async function updateSettings( +export const updateSettings = async ( newSettings: Record, filePath: string = path.join(HOME_DIR, '.wakatime.cfg'), -): Promise { +): Promise => { try { let config: IniConfig diff --git a/packages/server/utils/localAuth.util.ts b/packages/server/utils/localAuth.util.ts index c3a4842d..4bafedc8 100644 --- a/packages/server/utils/localAuth.util.ts +++ b/packages/server/utils/localAuth.util.ts @@ -19,7 +19,7 @@ import { CodeClimberError } from './codeClimberErrors' import { existsSync } from 'node:fs' import { v4 as uuidv4 } from 'uuid' -export async function isValidLocalApiKey(apiKey: string): Promise { +export const isValidLocalApiKey = async (apiKey: string): Promise => { try { const iniConfig = await readIniFile(CODE_CLIMBER_INI_PATH) if (iniConfig.settings.local_api_key !== apiKey) { @@ -35,7 +35,7 @@ export async function isValidLocalApiKey(apiKey: string): Promise { } } -async function setLocalApiKey(apiKey: string): Promise { +export const setLocalApiKey = async (apiKey: string): Promise => { try { const iniConfig = await readIniFile(CODE_CLIMBER_INI_PATH) iniConfig.settings.local_api_key = apiKey @@ -47,7 +47,9 @@ async function setLocalApiKey(apiKey: string): Promise { } } -export async function getLocalApiKey(isAdmin = false): Promise { +export const getLocalApiKey = async ( + isAdmin = false, +): Promise => { try { if (!existsSync(CODE_CLIMBER_INI_PATH)) { const iniContent = stringifyIni({ settings: {} }) @@ -77,7 +79,9 @@ export async function getLocalApiKey(isAdmin = false): Promise { } } -export async function setLocalApiKeyReadable(readable: boolean): Promise { +export const setLocalApiKeyReadable = async ( + readable: boolean, +): Promise => { try { const iniConfig = await readIniFile(CODE_CLIMBER_INI_PATH) iniConfig.settings.local_api_key_readable = readable.toString() diff --git a/packages/server/utils/node.util.ts b/packages/server/utils/node.util.ts index 70274e58..eddbf441 100644 --- a/packages/server/utils/node.util.ts +++ b/packages/server/utils/node.util.ts @@ -116,7 +116,7 @@ class LinuxNodeUtil extends BaseNodeUtil { } } -function createNodeUtil(): INodeUtil { +const createNodeUtil = (): INodeUtil => { switch (process.platform) { case 'darwin': return new DarwinNodeUtil() @@ -158,5 +158,4 @@ const logPaths = () => { logPaths() -// Default export -export default nodeUtil +export { nodeUtil } diff --git a/packages/server/utils/sqlReader.util.ts b/packages/server/utils/sqlReader.util.ts index 057673e0..ef50c2a1 100644 --- a/packages/server/utils/sqlReader.util.ts +++ b/packages/server/utils/sqlReader.util.ts @@ -5,10 +5,10 @@ import { dirname, join } from 'path' const cache: Record = {} // Utility function to read file content and return it as a string -async function getFileContentAsString( +const getFileContentAsString = async ( fileName: string, additionalPath = 'queries', -) { +) => { try { // Dynamically determine the directory of the caller // Create a new Error and use its stack trace @@ -44,6 +44,4 @@ async function getFileContentAsString( } } -export default { - getFileContentAsString, -} +export { getFileContentAsString } diff --git a/scripts/mock_install.sh b/scripts/mock_install.sh index 19befd4d..c145891f 100644 --- a/scripts/mock_install.sh +++ b/scripts/mock_install.sh @@ -8,7 +8,14 @@ if [ $# -eq 0 ]; then fi VERSION=$1 +RUN_MODE=false +# Check for --run flag +if [[ "$2" == "--run" ]]; then + RUN_MODE=true +fi + +git checkout v$VERSION # Get the directory of the script SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" @@ -19,15 +26,26 @@ cd "$PROJECT_ROOT" # Create the package npm pack -# Create temp directory -TEMP_DIR=$(mktemp -d) -cd $TEMP_DIR +# Create temp directory or real directory based on RUN_MODE +if [ "$RUN_MODE" = true ]; then + INSTALL_DIR="$(dirname "$PROJECT_ROOT")/mock-codeclimbers/codeclimbers_install_$VERSION" + mkdir -p "$INSTALL_DIR" +else + INSTALL_DIR=$(mktemp -d) +fi +cd "$INSTALL_DIR" # Create package.json with dynamic version echo "{\"dependencies\":{\"codeclimbers\":\"file:$PROJECT_ROOT/codeclimbers-$VERSION.tgz\"}}" > package.json # set environment variable -export CODECLIMBERS_MOCK_INSTALL=true +if [ "$RUN_MODE" = true ]; then + export CODECLIMBERS_MOCK_INSTALL=false +else + export NODE_ENV=development + export CODECLIMBERS_MOCK_INSTALL=true +fi + # Install npm install @@ -39,11 +57,14 @@ node node_modules/codeclimbers/bin/run.js start EXIT_STATUS=$? # Clean up -cd "$PROJECT_ROOT" -rm -rf $TEMP_DIR - -# Remove the .tgz file -rm codeclimbers-$VERSION.tgz +if [ "$RUN_MODE" = false ]; then + cd "$PROJECT_ROOT" + rm -rf "$INSTALL_DIR" + rm "codeclimbers-$VERSION.tgz" +else + echo "Installation completed in: $INSTALL_DIR" + echo "The .tgz file is still in the project root directory." +fi # Exit with the status from the codeclimbers execution exit $EXIT_STATUS \ No newline at end of file