diff --git a/index.html b/index.html index 1ea5a8d..5ae7447 100644 --- a/index.html +++ b/index.html @@ -5,10 +5,10 @@ VA Tracker - + /> -->
diff --git a/package-lock.json b/package-lock.json index 016bdbf..d3f1177 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "va-tracker", "version": "0.0.0", "dependencies": { + "@reduxjs/toolkit": "^2.10.1", "@tailwindcss/vite": "^4.1.16", "axios": "^1.13.2", "chalk": "^5.6.2", @@ -17,6 +18,7 @@ "os-utils": "^0.0.14", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-redux": "^9.2.0", "react-router-dom": "^7.9.5", "recharts": "^2.12.7", "sharp": "^0.34.4", @@ -79,6 +81,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.2", @@ -2068,6 +2071,32 @@ "node": ">=18" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.10.1.tgz", + "integrity": "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.2.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.22.4", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", @@ -2272,6 +2301,18 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -2747,13 +2788,14 @@ "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "dev": true + "devOptional": true }, "node_modules/@types/react": { "version": "18.3.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.8.tgz", "integrity": "sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==", - "dev": true, + "devOptional": true, + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -2777,6 +2819,12 @@ "@types/node": "*" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/verror": { "version": "1.10.10", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.10.tgz", @@ -2983,6 +3031,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3171,7 +3220,6 @@ "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", "dev": true, - "peer": true, "dependencies": { "archiver-utils": "^2.1.0", "async": "^3.2.4", @@ -3190,7 +3238,6 @@ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", "dev": true, - "peer": true, "dependencies": { "glob": "^7.1.4", "graceful-fs": "^4.2.0", @@ -3212,7 +3259,6 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, - "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -3227,15 +3273,13 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "peer": true + "dev": true }, "node_modules/archiver-utils/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, - "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -3479,6 +3523,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001587", "electron-to-chromium": "^1.4.668", @@ -4049,7 +4094,6 @@ "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", "dev": true, - "peer": true, "dependencies": { "buffer-crc32": "^0.2.13", "crc32-stream": "^4.0.2", @@ -4231,7 +4275,6 @@ "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", "dev": true, - "peer": true, "bin": { "crc32": "bin/crc32.njs" }, @@ -4244,7 +4287,6 @@ "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", "dev": true, - "peer": true, "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^3.4.0" @@ -4656,6 +4698,7 @@ "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.0.5.tgz", "integrity": "sha512-ocnZV44ZqInoSFaY54fF7BlCtw+WtbrjyPrkBhaB+Ztn7GPKjmFgRbIKytifJ8h9Cib8jdFRMgjCUtkU45Y6DA==", "dev": true, + "peer": true, "dependencies": { "app-builder-lib": "25.0.5", "builder-util": "25.0.3", @@ -4879,7 +4922,6 @@ "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.0.5.tgz", "integrity": "sha512-N2U7LGSdt4hmEhjEeIV2XJbjj2YIrTL6enfsGKfOhGTpL6GEejUmT3gjdKUqKBS5+NBx0GWhnEwD3MpO2P6Nfg==", "dev": true, - "peer": true, "dependencies": { "app-builder-lib": "25.0.5", "archiver": "^5.3.1", @@ -4892,7 +4934,6 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, - "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -4907,7 +4948,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, - "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -4920,7 +4960,6 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, - "peer": true, "engines": { "node": ">= 10.0.0" } @@ -5202,15 +5241,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "devOptional": true }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -5633,8 +5663,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "peer": true + "dev": true }, "node_modules/fs-extra": { "version": "8.1.0", @@ -6540,6 +6569,16 @@ } ] }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -6887,8 +6926,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/isbinaryfile": { "version": "5.0.2", @@ -7149,7 +7187,6 @@ "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", "dev": true, - "peer": true, "dependencies": { "readable-stream": "^2.0.5" }, @@ -7162,7 +7199,6 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, - "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -7177,15 +7213,13 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "peer": true + "dev": true }, "node_modules/lazystream/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, - "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -7463,36 +7497,31 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/lodash.difference": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/lodash.flatten": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", - "dev": true, - "peer": true + "dev": true }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/log-symbols": { "version": "4.1.0", @@ -8132,7 +8161,6 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -8757,8 +8785,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "peer": true + "dev": true }, "node_modules/progress": { "version": "2.0.3", @@ -8839,6 +8866,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -8850,6 +8878,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -8863,6 +8892,30 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -9001,7 +9054,6 @@ "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", "dev": true, - "peer": true, "dependencies": { "minimatch": "^5.1.0" } @@ -9011,7 +9063,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, - "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -9049,6 +9100,22 @@ "decimal.js-light": "^2.4.1" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT", + "peer": true + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -9107,6 +9174,12 @@ "url": "https://github.com/sponsors/jet2jet" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -9967,7 +10040,6 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, - "peer": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -10337,6 +10409,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utf8-byte-length": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", @@ -10399,6 +10480,7 @@ "version": "5.4.7", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz", "integrity": "sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ==", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -10831,7 +10913,6 @@ "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", "dev": true, - "peer": true, "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", @@ -10846,7 +10927,6 @@ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", "dev": true, - "peer": true, "dependencies": { "glob": "^7.2.3", "graceful-fs": "^4.2.0", diff --git a/package.json b/package.json index dd4f5ca..885d119 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "test:unit": "vitest src" }, "dependencies": { + "@reduxjs/toolkit": "^2.10.1", "@tailwindcss/vite": "^4.1.16", "axios": "^1.13.2", "chalk": "^5.6.2", @@ -27,6 +28,7 @@ "os-utils": "^0.0.14", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-redux": "^9.2.0", "react-router-dom": "^7.9.5", "recharts": "^2.12.7", "sharp": "^0.34.4", diff --git a/src/electron/ipc/ipcHandler.ts b/src/electron/ipc/ipcHandler.ts index e79000c..14ef2b8 100644 --- a/src/electron/ipc/ipcHandler.ts +++ b/src/electron/ipc/ipcHandler.ts @@ -1,54 +1,55 @@ -// electron/ipc/tokenHandler.ts -import { ipcMain } from 'electron'; -import { getToken, saveToken, clearToken } from '../utils/tokenHelper.js'; -import { getActiveApp, handleLogin, takeScreenshot } from '../utils/utilityFunctions.js'; -import api from '../api/axiosClient.js'; +import { ipcMain, BrowserWindow } from "electron"; +import { getToken, saveToken, clearToken } from "../utils/tokenHelper.js"; +import { getActiveApp, handleLogin, takeScreenshot, startWindowPolling } from "../utils/utilityFunctions.js"; +import api from "../api/axiosClient.js"; import { getMainWindow } from '../main.js'; import { exec } from 'child_process'; -ipcMain.handle('get-token', async (_, TOKEN_KEY: "ACCESS_TOKEN" | "REFRESH_TOKEN") => { - return await getToken(TOKEN_KEY); -}); +export const registerIPCHandlers = (mainWindow: BrowserWindow) => { + console.log("โš™๏ธ Registering IPC Handlers..."); -ipcMain.handle('save-token', async (_, TOKEN_KEY: "ACCESS_TOKEN" | "REFRESH_TOKEN", token: string) => { - await saveToken(TOKEN_KEY, token); + ipcMain.handle("get-token", async (_, key: "ACCESS_TOKEN" | "REFRESH_TOKEN") => { + return await getToken(key); + }); + + ipcMain.handle("save-token", async (_, key: "ACCESS_TOKEN" | "REFRESH_TOKEN", token: string) => { + await saveToken(key, token); return true; -}); + }); -ipcMain.handle('clear-token', async (_, TOKEN_KEY: string) => { - await clearToken(TOKEN_KEY); + ipcMain.handle("clear-token", async (_, key: string) => { + await clearToken(key); return true; -}); + }); + ipcMain.handle("upload-screenshot", async () => { + await takeScreenshot(); + return { success: true }; + }); -ipcMain.handle('upload-screenshot', async () => { - takeScreenshot() -}) - -// Handle login request from renderer -ipcMain.handle('login-request', async (event, credentials: { email: string, password: string }) => { + ipcMain.handle("login-request", async (_, credentials: { email: string; password: string }) => { return handleLogin(credentials); -}); + }); -ipcMain.handle('logout-request', async () => { + ipcMain.handle("logout-request", async () => { try { - console.log("Handle Logout listened") - console.log("Token", await getToken("ACCESS_TOKEN")) - const res = await api.get("/logout"); - if (res && ("success" in res) && res.success) { - return { success: true, message: "Logout Successfully" } - } - return { success: false, message: "" } - } catch (error) { - return { success: false, message: "" } + console.log("Handle Logout listened"); + console.log("Token", await getToken("ACCESS_TOKEN")); + const res = await api.get("/logout"); + if (res && "success" in res && res.success) { + return { success: true, message: "Logout Successfully" }; + } + return { success: false, message: "" }; + } catch { + return { success: false, message: "Logout failed" }; } -}); + }); -ipcMain.handle('get-active-app', () => { - return getActiveApp(); -}); + ipcMain.handle("get-active-app", async () => { + return await getActiveApp(); + }); -ipcMain.handle('toggle-close-btn', (event, shouldEnable: boolean) => { + ipcMain.handle('toggle-close-btn', (event, shouldEnable: boolean) => { const mainWindow = getMainWindow() console.log("toggle-close-btn Called", mainWindow) if (mainWindow && !mainWindow.isDestroyed()) { @@ -59,26 +60,11 @@ ipcMain.handle('toggle-close-btn', (event, shouldEnable: boolean) => { ipcMain.handle('resize-window', (event, width: number, height: number) => { const mainWindow = getMainWindow() console.log("toggle-close-btn Called", mainWindow) - if (mainWindow && !mainWindow.isDestroyed()) { + // if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.setSize(width, height); - } -}); + // } + }) -ipcMain.handle('get-running-apps', async () => { - return new Promise((resolve, reject) => { - exec('tasklist', (err, stdout) => { - if (err) return reject(err); - - const lines = stdout.split('\n').slice(3); // skip headers - const apps = lines - .map(line => line.trim().split(/\s+/)) - .filter(parts => parts.length > 1) - .map(parts => ({ - name: parts[0], - pid: parts[1], - })); - - resolve(apps); - }); - }); -}); \ No newline at end of file + // โœ… Start background polling when main window is ready +}; +// startWindowPolling(mainWindow); diff --git a/src/electron/main.ts b/src/electron/main.ts index 1af0289..b05c915 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -4,6 +4,7 @@ import { getStaticData, pollResources } from './resourceManager.js'; import { getPreloadPath, getUIPath } from './pathResolver.js'; import "./ipc/ipcHandler.js"; +import { registerIPCHandlers } from './ipc/ipcHandler.js'; let mainWindow:any = null; @@ -14,7 +15,7 @@ app.on('ready', async () => { width: 500, // fixed width height: 600, // fixed height - resizable: false, // prevents resizing + resizable: true, // prevents resizing fullscreenable: false, // disables fullscreen toggle (F11 or double-click title bar) minimizable: false, // disables minimize button maximizable: false, // prevents maximize @@ -41,6 +42,10 @@ app.on('ready', async () => { } // pollResources(mainWindow); + // ๐Ÿ”น Register all IPC handlers (includes polling) + registerIPCHandlers(mainWindow); + + pollResources(mainWindow); // IPC handlers // ipcMainHandle('getStaticData', () => getStaticData()); diff --git a/src/electron/preload.cts b/src/electron/preload.cts index a2272b2..ce7e2de 100644 --- a/src/electron/preload.cts +++ b/src/electron/preload.cts @@ -10,7 +10,8 @@ contextBridge.exposeInMainWorld('electron', { getActiveAppDetails: () => ipcRenderer.invoke('get-active-app'), toggleCloseBtn: (shouldEnable: boolean) => ipcRenderer.invoke('toggle-close-btn', shouldEnable), resizeWindow: (width: number, height: number) => ipcRenderer.invoke('resize-window', width, height), - getRunningApps: () => ipcRenderer.invoke('get-running-apps'), + onWindowUpdate: (callback) => + ipcRenderer.on("window:updated", (_, data) => callback(data)), } satisfies Window['electron']); // function ipcInvoke( diff --git a/src/electron/utils/utilityFunctions.ts b/src/electron/utils/utilityFunctions.ts index b38a090..c420837 100644 --- a/src/electron/utils/utilityFunctions.ts +++ b/src/electron/utils/utilityFunctions.ts @@ -1,4 +1,4 @@ -import { desktopCapturer } from "electron"; +import { BrowserWindow, desktopCapturer } from "electron"; import * as fs from "fs"; import path from "path"; import axios from 'axios'; @@ -95,7 +95,23 @@ export const handleLogin = async (credentials: { email: string, password: string } } +// Get active window info with timestamp export const getActiveApp = async () => { - const result = await activeWindow(); - return result; + const result = await activeWindow(); + const dataWithTime = { ...result, timestamp: new Date().toISOString() }; +// console.log("๐ŸชŸ Active Window with Time:", dataWithTime); + return dataWithTime; +}; + +// Periodically send active window updates +export const startWindowPolling = (win: BrowserWindow) => { + console.log("โฑ๏ธ Started active window polling (every 5s)"); + setInterval(async () => { + try { + const app = await getActiveApp(); + if (app) win.webContents.send("window:updated", app); + } catch (err) { + console.error("โŒ Polling error:", err); + } + }, 5000); }; \ No newline at end of file diff --git a/src/ui/hooks/useActiveAppTracker.tsx b/src/ui/hooks/useActiveAppTracker.tsx new file mode 100644 index 0000000..e2568b3 --- /dev/null +++ b/src/ui/hooks/useActiveAppTracker.tsx @@ -0,0 +1,51 @@ +// src/hooks/useActiveAppTracker.ts +import { useEffect, useRef } from "react"; +import { useDispatch } from "react-redux"; +import { setActiveWindow, WindowInfo } from "../store/windowSlice"; + +export const useActiveAppTracker = ( + isTracking: boolean, + projectId: string, + projectName: string +) => { + const dispatch = useDispatch(); + const lastActiveRef = useRef(null); + + useEffect(() => { + if (!isTracking || !projectId) { + console.log(" Tracking not active or project not selected"); + return; + } + + console.log(` Tracking started for project: ${projectName}`); + + const fetchActiveApp = async () => { + try { + const details = (await window.electron.getActiveAppDetails()) as WindowInfo; + if (!details || !details.id) return; + + const prev = lastActiveRef.current; + const hasChanged = + !prev || + prev.title !== details.title || + prev.owner.processId !== details.owner.processId; + + if (hasChanged) { + console.log("๐ŸชŸ Window changed:", details.title); + dispatch(setActiveWindow({ window: details, projectId, projectName })); + lastActiveRef.current = details; + } + } catch (error) { + console.error("โŒ Failed to fetch active app:", error); + } + }; + + fetchActiveApp(); + const intervalId = setInterval(fetchActiveApp, 5000); + + return () => { + console.log("๐Ÿ›‘ Stopping active window tracker"); + clearInterval(intervalId); + }; + }, [isTracking, dispatch, projectId, projectName]); +}; diff --git a/src/ui/layout/DashboardLayout.tsx b/src/ui/layout/DashboardLayout.tsx index c6cf96e..242b7d7 100644 --- a/src/ui/layout/DashboardLayout.tsx +++ b/src/ui/layout/DashboardLayout.tsx @@ -22,7 +22,11 @@ import { ShieldBan, ChevronDown, Play, + ChevronUp, } from "lucide-react"; +import { useActiveAppTracker } from "../hooks/useActiveAppTracker"; +import { useSelector } from "react-redux"; +import { RootState } from "../store"; interface User { email: string; @@ -43,7 +47,7 @@ interface ProjectTrackingData { isPaused: boolean; pauseStartTime: Date | null; totalPausedTime: number; - accumulatedTime: number; // โœ… NEW: Stores time from previous sessions + accumulatedTime: number; // NEW: Stores time from previous sessions } interface TrackingState { @@ -67,11 +71,17 @@ interface DashboardLayoutProps { } const DashboardLayout: React.FC = ({ setUser }) => { + const [isSidebarOpen, setSidebarOpen] = useState(true); const [isMobile, setIsMobile] = useState(window.innerWidth < 768); const navigate = useNavigate(); const location = useLocation(); + const { windowLogs } = useSelector((state: RootState) => state.window); + + console.log("๐Ÿงพ windowLogs in component:", windowLogs); + + // Tracking states const [selectedProject, setSelectedProject] = useState(null); const [isTracking, setIsTracking] = useState(false); @@ -81,6 +91,8 @@ const DashboardLayout: React.FC = ({ setUser }) => { const [endTime, setEndTime] = useState(null); const timerRef = useRef | null>(null); + useActiveAppTracker(isTracking, selectedProject?.id, selectedProject?.projectName); + const [projectsData, setProjectsData] = useState([ { id: 1, projectName: "CRM Redesign", clientName: "Salesforce" }, { id: 2, projectName: "AI Assistant", clientName: "OpenAI" }, @@ -105,21 +117,6 @@ const DashboardLayout: React.FC = ({ setUser }) => { }; - - useEffect(() => { - setInterval(async () => { - const details = await window.electron.getActiveAppDetails() - console.log(details) - }, 15000) - }, []) - - - const toggleSidebar = () => setSidebarOpen(!isSidebarOpen); - - const closeSidebarOnMobile = () => { - if (isMobile) setSidebarOpen(false); - }; - const menuItems = [ { label: "Dashboard", path: "/dashboard", icon: , exact: true }, { label: "Projects", path: "/dashboard/projects", icon: }, @@ -129,7 +126,7 @@ const DashboardLayout: React.FC = ({ setUser }) => { { label: "Help & Support", path: "/dashboard/help-support", icon: }, ]; - // โœ… Sync global states with selected project tracking data + // Sync global states with selected project tracking data useEffect(() => { if (selectedProject) { const projectData = projectTrackingMap.get(selectedProject.id); @@ -143,7 +140,7 @@ const DashboardLayout: React.FC = ({ setUser }) => { } }, [selectedProject, projectTrackingMap]); - // โœ… Timer effect - calculates elapsed time based on timestamps + accumulated time + // Timer effect - calculates elapsed time based on timestamps + accumulated time useEffect(() => { if (isTracking && !isPaused && selectedProject) { timerRef.current = setInterval(() => { @@ -156,9 +153,9 @@ const DashboardLayout: React.FC = ({ setUser }) => { const startTimestamp = current.startTime.getTime(); const currentSessionTime = Math.floor((now - startTimestamp) / 1000); const totalPausedTime = current.totalPausedTime || 0; - - // โœ… FIX: Add accumulated time from previous sessions - const calculatedElapsed = + + // FIX: Add accumulated time from previous sessions + const calculatedElapsed = (current.accumulatedTime || 0) + currentSessionTime - totalPausedTime; current.elapsedTime = calculatedElapsed; @@ -185,7 +182,7 @@ const DashboardLayout: React.FC = ({ setUser }) => { }; }, [isTracking, isPaused, selectedProject]); - // โœ… Start tracking + // Start tracking const handleStartTracking = (project: Project) => { if (timerRef.current) { clearInterval(timerRef.current); @@ -193,7 +190,7 @@ const DashboardLayout: React.FC = ({ setUser }) => { } const now = new Date(); - + window.electron.toggleCloseBtn(false) setProjectTrackingMap((prev) => { const updated = new Map(prev); const current = updated.get(project.id) || { @@ -207,7 +204,7 @@ const DashboardLayout: React.FC = ({ setUser }) => { accumulatedTime: 0, }; - // โœ… Keep accumulated time from previous sessions + // Keep accumulated time from previous sessions current.startTime = now; current.isTracking = true; current.isPaused = false; @@ -225,7 +222,7 @@ const DashboardLayout: React.FC = ({ setUser }) => { }); }; - // โœ… Pause/Resume tracking with pause duration tracking + // Pause/Resume tracking with pause duration tracking const handlePauseResume = () => { if (!selectedProject) return; @@ -263,7 +260,7 @@ const DashboardLayout: React.FC = ({ setUser }) => { }); }; - // โœ… Stop tracking - PRESERVES elapsed time + // Stop tracking - PRESERVES elapsed time const handleStopTracking = () => { if (!selectedProject) return; @@ -273,13 +270,13 @@ const DashboardLayout: React.FC = ({ setUser }) => { } const now = new Date(); - + window.electron.toggleCloseBtn(true) setProjectTrackingMap((prev) => { const updated = new Map(prev); const current = updated.get(selectedProject.id); if (!current) return updated; - // โœ… FIX: Store current elapsed time as accumulated time + // FIX: Store current elapsed time as accumulated time current.accumulatedTime = current.elapsedTime; current.isTracking = false; current.isPaused = false; @@ -291,7 +288,7 @@ const DashboardLayout: React.FC = ({ setUser }) => { setIsTracking(false); setIsPaused(false); setEndTime(now); - setElapsedTime(current.elapsedTime); // โœ… Keep the elapsed time + setElapsedTime(current.elapsedTime); // Keep the elapsed time return updated; }); @@ -308,7 +305,7 @@ const DashboardLayout: React.FC = ({ setUser }) => { }; }, []); - // โœ… Handle project selection + // Handle project selection const handleProjectSelect = (id: number) => { if (isTracking) return; @@ -355,6 +352,8 @@ const DashboardLayout: React.FC = ({ setUser }) => { return `${hrs}h ${mins}m ${secs}s`; }; + const toggleWindowSize = useRef(true); // true -> Open, false -> Close + return ( <> {/* Header */} @@ -413,22 +412,20 @@ const DashboardLayout: React.FC = ({ setUser }) => { {/* Dropdown */} -
-
- -
- -
+ @@ -445,10 +442,9 @@ const DashboardLayout: React.FC = ({ setUser }) => { to={item.path} end={item.exact} className={({ isActive }) => - `flex items-center justify-center w-10 h-10 rounded-lg transition-all duration-150 cursor-pointer ${ - isActive - ? "bg-primary text-primary-content" - : "hover:bg-base-300 text-base-content" + `flex items-center justify-center w-10 h-10 rounded-lg transition-all duration-150 cursor-pointer ${isActive + ? "bg-primary text-primary-content" + : "hover:bg-base-300 text-base-content" }` } > diff --git a/src/ui/main.tsx b/src/ui/main.tsx index 791f139..9171cd7 100644 --- a/src/ui/main.tsx +++ b/src/ui/main.tsx @@ -2,9 +2,11 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' import './index.css' +import { Provider } from "react-redux"; +import { store } from "./store"; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - + - , + ) diff --git a/src/ui/pages/Dashboard.tsx b/src/ui/pages/Dashboard.tsx index 537b808..f3842fa 100644 --- a/src/ui/pages/Dashboard.tsx +++ b/src/ui/pages/Dashboard.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useRef } from "react"; import { Clock, TrendingUp, Play, Square, ProjectorIcon, Pause } from "lucide-react"; import { useOutletContext } from "react-router-dom"; @@ -91,6 +91,9 @@ const Dashboard: React.FC = () => { ? projectTrackingMap.get(selectedProject.id) : null; + + + return (
{/* Header */} @@ -104,35 +107,6 @@ const Dashboard: React.FC = () => { > Screenshot Test - - - - {/* Widgets */} diff --git a/src/ui/pages/Timesheets/Timesheets.tsx b/src/ui/pages/Timesheets/Timesheets.tsx index 7bf0563..c6d6944 100644 --- a/src/ui/pages/Timesheets/Timesheets.tsx +++ b/src/ui/pages/Timesheets/Timesheets.tsx @@ -1,5 +1,7 @@ import React from "react"; import { CalendarDays, Clock, FileText, CheckCircle, XCircle } from "lucide-react"; +import { useSelector } from "react-redux"; +import { RootState } from "../../store"; interface TimesheetEntry { id: number; @@ -22,6 +24,10 @@ const Timesheets: React.FC = () => { const totalHours = timesheetData.reduce((acc, t) => acc + t.hours, 0); const approvedCount = timesheetData.filter((t) => t.status === "Approved").length; + const { windowLogs } = useSelector((state: RootState) => state.window); + + console.log("๐Ÿงพ Timesheet in component:", windowLogs); + return (
{/* Header */} diff --git a/src/ui/store/index.tsx b/src/ui/store/index.tsx new file mode 100644 index 0000000..4b7745d --- /dev/null +++ b/src/ui/store/index.tsx @@ -0,0 +1,12 @@ +// src/store/index.ts +import { configureStore } from "@reduxjs/toolkit"; +import windowReducer from "./windowSlice"; + +export const store = configureStore({ + reducer: { + window: windowReducer, + }, +}); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; diff --git a/src/ui/store/windowSlice.tsx b/src/ui/store/windowSlice.tsx new file mode 100644 index 0000000..6b8d57f --- /dev/null +++ b/src/ui/store/windowSlice.tsx @@ -0,0 +1,128 @@ +// src/store/windowSlice.ts +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +export interface OwnerInfo { + processId: number; + path: string; + name: string; +} + +export interface WindowInfo { + platform: string; + id: number; + title: string; + owner: OwnerInfo; + bounds: Record; + contentBounds: Record; + memoryUsage: number; + timestamp: string; +} + +export interface ProjectWindowLog { + projectId: string; + projectName: string; + appName: string; + windowTitle: string; + platform: string; + duration: number; // in seconds + startTime: string; + endTime: string; + memoryUsage: number; +} + +interface WindowState { + activeWindow: WindowInfo | null; + activeSince: number | null; + activeProjectId: string | null; + activeProjectName: string | null; + windowLogs: ProjectWindowLog[]; +} + +const initialState: WindowState = { + activeWindow: null, + activeSince: null, + activeProjectId: null, + activeProjectName: null, + windowLogs: [], +}; + +interface SetActiveWindowPayload { + window: WindowInfo; + projectId: string; + projectName: string; +} + +const windowSlice = createSlice({ + name: "window", + initialState, + reducers: { + // โœ… Start tracking window for a project + setActiveWindow: (state, action: PayloadAction) => { + const { window, projectId, projectName } = action.payload; + const now = Date.now(); + + // ๐Ÿ•’ Log previous window before switching + if (state.activeWindow && state.activeSince && state.activeProjectId) { + const duration = Math.floor((now - state.activeSince) / 1000); + + const logEntry: ProjectWindowLog = { + projectId: state.activeProjectId, + projectName: state.activeProjectName || "Untitled", + appName: state.activeWindow.owner.name, + windowTitle: state.activeWindow.title, + platform: state.activeWindow.platform, + duration, + startTime: new Date(state.activeSince).toISOString(), + endTime: new Date(now).toISOString(), + memoryUsage: state.activeWindow.memoryUsage, + }; + + state.windowLogs.push(logEntry); + console.log("๐Ÿงพ Log saved:", logEntry); + } + + // ๐Ÿ” Update new active window + project context + state.activeWindow = window; + state.activeSince = now; + state.activeProjectId = projectId; + state.activeProjectName = projectName; + }, + + // โœ… Stop tracking (finalize) + finalizeActiveWindow: (state) => { + if (state.activeWindow && state.activeSince && state.activeProjectId) { + const now = Date.now(); + const duration = Math.floor((now - state.activeSince) / 1000); + + const logEntry: ProjectWindowLog = { + projectId: state.activeProjectId, + projectName: state.activeProjectName || "Untitled", + appName: state.activeWindow.owner.name, + windowTitle: state.activeWindow.title, + platform: state.activeWindow.platform, + duration, + startTime: new Date(state.activeSince).toISOString(), + endTime: new Date(now).toISOString(), + memoryUsage: state.activeWindow.memoryUsage, + }; + + state.windowLogs.push(logEntry); + console.log("โœ… Final log saved:", logEntry); + } + + state.activeWindow = null; + state.activeSince = null; + state.activeProjectId = null; + state.activeProjectName = null; + }, + + clearWindowLogs: (state) => { + state.windowLogs = []; + }, + }, +}); + +export const { setActiveWindow, finalizeActiveWindow, clearWindowLogs } = + windowSlice.actions; + +export default windowSlice.reducer; diff --git a/types.d.ts b/types.d.ts index 7a027ad..60eef43 100644 --- a/types.d.ts +++ b/types.d.ts @@ -32,7 +32,7 @@ interface Window { getActiveAppDetails: () => {}; toggleCloseBtn: (shouldEnable: boolean) => {}, resizeWindow: (width: number, height: number) => {}, - getRunningApps: () => Promise<{name: string, pid:string}[]>, + onWindowUpdate: (callback: (data: any) => void) => void; }; }