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 00000000..3e5c82af Binary files /dev/null and b/packages/app/src/assets/icons/boss.png differ 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