From 5bd28ba80a7fd94fa5c4e4b337f2d0c3a66faece Mon Sep 17 00:00:00 2001 From: FalkenDev Date: Mon, 4 May 2026 12:17:45 +0200 Subject: [PATCH 01/16] fix: change 'dev' to 'devOptional' for multiple dependencies in package-lock.json --- frontend/package-lock.json | 134 +++++++------------------------------ 1 file changed, 24 insertions(+), 110 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5d5d6cc..fa645ac 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1642,7 +1642,7 @@ "version": "2.11.0", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", - "dev": true, + "devOptional": true, "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@canvas/image-data": { @@ -1668,7 +1668,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1685,7 +1684,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1702,7 +1700,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1719,7 +1716,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1736,7 +1732,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1753,7 +1748,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1770,7 +1764,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1787,7 +1780,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1804,7 +1796,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1821,7 +1812,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1838,7 +1828,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1855,7 +1844,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1872,7 +1860,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1889,7 +1876,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1906,7 +1892,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1923,7 +1908,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1940,7 +1924,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1957,7 +1940,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1974,7 +1956,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1991,7 +1972,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2008,7 +1988,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2025,7 +2004,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2042,7 +2020,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2059,7 +2036,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2076,7 +2052,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2093,7 +2068,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2845,7 +2819,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -2867,7 +2841,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -2877,7 +2851,7 @@ "version": "0.3.11", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -2894,7 +2868,7 @@ "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -3189,7 +3163,6 @@ "version": "2.5.6", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -3229,7 +3202,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3250,7 +3222,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3271,7 +3242,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3292,7 +3262,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3313,7 +3282,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3334,7 +3302,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3355,7 +3322,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3376,7 +3342,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3397,7 +3362,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3418,7 +3382,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3439,7 +3402,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3460,7 +3422,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3481,7 +3442,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3608,7 +3568,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3622,7 +3581,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3636,7 +3594,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3650,7 +3607,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3664,7 +3620,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3678,7 +3633,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3692,7 +3646,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3706,7 +3659,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3720,7 +3672,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3734,7 +3685,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3748,7 +3698,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3762,7 +3711,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3776,7 +3724,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3790,7 +3737,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3804,7 +3750,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3818,7 +3763,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3832,7 +3776,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3846,7 +3789,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3860,7 +3802,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3874,7 +3815,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3888,7 +3828,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3902,7 +3841,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3916,7 +3854,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3930,7 +3867,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3944,7 +3880,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4058,7 +3993,7 @@ "version": "22.19.15", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -5138,7 +5073,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/cac": { @@ -5271,7 +5206,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "readdirp": "^4.0.1" @@ -5334,14 +5269,14 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/common-tags": { @@ -5621,7 +5556,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -6499,7 +6434,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -6904,7 +6838,7 @@ "version": "5.1.5", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/import-fresh": { @@ -7997,7 +7931,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, "license": "MIT", "optional": true }, @@ -9065,7 +8998,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 14.18.0" @@ -9387,7 +9320,7 @@ "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" @@ -9473,7 +9406,6 @@ "version": "1.98.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz", "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -9495,7 +9427,7 @@ "version": "1.98.0", "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.98.0.tgz", "integrity": "sha512-Do7u6iRb6K+lrllcTkB1BXcHwOxcKe3rEfOF/GcCLE2w3WpddakRAosJOHFUR37DpsvimQXEt5abs3NzUjEIqg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@bufbuild/protobuf": "^2.5.0", @@ -9543,7 +9475,6 @@ "!riscv64", "!x64" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -9557,7 +9488,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9574,7 +9504,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9591,7 +9520,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9608,7 +9536,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9625,7 +9552,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9642,7 +9568,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9659,7 +9584,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9676,7 +9600,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9693,7 +9616,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9710,7 +9632,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9727,7 +9648,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9744,7 +9664,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9761,7 +9680,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9778,7 +9696,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9792,7 +9709,6 @@ "version": "1.98.0", "resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.98.0.tgz", "integrity": "sha512-C4MMzcAo3oEDQnW7L8SBgB9F2Fq5qHPnaYTZRMOH3Mp/7kM4OooBInXpCiiFjLnjY95hzP4KyctVx0uYR6MYlQ==", - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9812,7 +9728,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9829,7 +9744,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9843,7 +9757,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -10173,7 +10087,7 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -10184,7 +10098,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -10425,7 +10339,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "sync-message-port": "^1.0.0" @@ -10438,7 +10352,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.2.0.tgz", "integrity": "sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=16.0.0" @@ -10477,7 +10391,7 @@ "version": "5.46.1", "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -10817,7 +10731,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -11229,7 +11143,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/vite": { @@ -12254,7 +12168,7 @@ "version": "2.8.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "yaml": "bin.mjs" From 746eb29847c41df4df6d0ee5ec1ca30bc1e46875 Mon Sep 17 00:00:00 2001 From: FalkenDev Date: Mon, 4 May 2026 12:18:21 +0200 Subject: [PATCH 02/16] bugfix: remove unnecessary sticky button wrapper (the btn was hidden before under safe-area) --- frontend/src/pages/WorkoutDetails.vue | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/frontend/src/pages/WorkoutDetails.vue b/frontend/src/pages/WorkoutDetails.vue index 953c3c3..07a6443 100644 --- a/frontend/src/pages/WorkoutDetails.vue +++ b/frontend/src/pages/WorkoutDetails.vue @@ -228,7 +228,7 @@ -
+
{{ $t('workout.startSession') }} @@ -435,13 +435,4 @@ const duplicate = async () => { .cursor-pointer { cursor: pointer; } - -.sticky-btn-wrapper { - position: fixed; - left: 0; - right: 0; - z-index: 10; - background: linear-gradient(180deg, rgba(12, 14, 18, 0) 0%, rgba(12, 14, 18, 1) 40%); - padding-top: 24px !important; -} From 76dbcd77e143264e701c87dcb7c3bd2e331273cc Mon Sep 17 00:00:00 2001 From: FalkenDev Date: Mon, 4 May 2026 12:21:03 +0200 Subject: [PATCH 03/16] fix: update styling for exercise details display in WorkoutExerciseCard from blue to green --- frontend/src/components/Session/WorkoutExerciseCard.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Session/WorkoutExerciseCard.vue b/frontend/src/components/Session/WorkoutExerciseCard.vue index b734708..71e8c6c 100644 --- a/frontend/src/components/Session/WorkoutExerciseCard.vue +++ b/frontend/src/components/Session/WorkoutExerciseCard.vue @@ -74,15 +74,15 @@
mdi-lightbulb-outline -

+

{{ resolvedExercise ? displayDescription(resolvedExercise) : '' }}

From 26c059ab2414f9fd36b7541117784087c2429eac Mon Sep 17 00:00:00 2001 From: FalkenDev Date: Mon, 4 May 2026 12:25:06 +0200 Subject: [PATCH 04/16] feat: add icon to allow adding a new set when not all sets are completed --- frontend/src/components/Session/WorkoutExerciseCard.vue | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/src/components/Session/WorkoutExerciseCard.vue b/frontend/src/components/Session/WorkoutExerciseCard.vue index 71e8c6c..e9b3dee 100644 --- a/frontend/src/components/Session/WorkoutExerciseCard.vue +++ b/frontend/src/components/Session/WorkoutExerciseCard.vue @@ -44,6 +44,12 @@ > mdi-timer-play-outline + + mdi-table-row-plus-after + Date: Mon, 4 May 2026 12:27:53 +0200 Subject: [PATCH 05/16] bugfix: update logic for showing exercise details based on workout set completion --- .../Session/WorkoutExerciseCard.vue | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/Session/WorkoutExerciseCard.vue b/frontend/src/components/Session/WorkoutExerciseCard.vue index e9b3dee..d72d46a 100644 --- a/frontend/src/components/Session/WorkoutExerciseCard.vue +++ b/frontend/src/components/Session/WorkoutExerciseCard.vue @@ -288,12 +288,6 @@ const allSetsDone = computed(() => { return props.workoutSets.every((set) => set.done); }); -watch(allSetsDone, (isCompleted) => { - if (isCompleted) { - showDetails.value = false; - } -}); - function handleRowClick(event: Event, { item }: { item: WorkoutSet }) { selectedSet.value = { ...item }; isEditDialogVisible.value = true; @@ -347,6 +341,7 @@ function deleteSet(setToDelete: WorkoutSet) { } function addSet() { + showDetails.value = true; emit('add:set'); } @@ -354,15 +349,9 @@ function onDoneChanged(set: WorkoutSet, isDone: boolean) { emit('update:set', { ...set, done: isDone }); } -watch( - allSetsDone, - (isCompleted) => { - if (isCompleted) { - showDetails.value = false; - } - }, - { immediate: true } -); +watch(allSetsDone, (isCompleted) => { + showDetails.value = !isCompleted; +}, { immediate: true }); From 6731ca53f5233852e1ed473c30d6f34db05797b6 Mon Sep 17 00:00:00 2001 From: FalkenDev Date: Mon, 4 May 2026 17:17:10 +0200 Subject: [PATCH 09/16] feat: implement streak freeze functionality with migration, service, and UI updates --- .../1775000000000-AddStreakFreezesToUser.ts | 57 +++++ backend/src/v1/user/user.controller.ts | 26 +- backend/src/v1/user/user.entity.ts | 9 + backend/src/v1/user/user.service.ts | 128 +++++++++- frontend/src/interfaces/User.interface.ts | 3 + frontend/src/locales/en.ts | 10 + frontend/src/locales/sv.ts | 10 + frontend/src/pages/Calendar.vue | 225 ++++++++++++++++-- frontend/src/pages/index.vue | 15 +- frontend/src/services/user.service.ts | 14 ++ 10 files changed, 459 insertions(+), 38 deletions(-) create mode 100644 backend/src/v1/migrations/1775000000000-AddStreakFreezesToUser.ts diff --git a/backend/src/v1/migrations/1775000000000-AddStreakFreezesToUser.ts b/backend/src/v1/migrations/1775000000000-AddStreakFreezesToUser.ts new file mode 100644 index 0000000..5e468a9 --- /dev/null +++ b/backend/src/v1/migrations/1775000000000-AddStreakFreezesToUser.ts @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2026 FalkenDev + * + * This file is part of Grindify. + * + * Grindify is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * You should have received a copy of the GNU Affero General Public + * License along with Grindify. If not, see + * . + */ + +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class AddStreakFreezesToUser1775000000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // Add streakFreezes column — users start with 1 freeze + await queryRunner.addColumn( + 'user', + new TableColumn({ + name: 'streakFreezes', + type: 'int', + default: 1, + }), + ); + + // Add streakFreezeUsedWeek column — ISO week key e.g. "2026-W18" + await queryRunner.addColumn( + 'user', + new TableColumn({ + name: 'streakFreezeUsedWeek', + type: 'varchar', + length: '10', + isNullable: true, + }), + ); + + // Add completedGoalWeeksCount — tracks how many goal-weeks have been completed + await queryRunner.addColumn( + 'user', + new TableColumn({ + name: 'completedGoalWeeksCount', + type: 'int', + default: 0, + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn('user', 'completedGoalWeeksCount'); + await queryRunner.dropColumn('user', 'streakFreezeUsedWeek'); + await queryRunner.dropColumn('user', 'streakFreezes'); + } +} diff --git a/backend/src/v1/user/user.controller.ts b/backend/src/v1/user/user.controller.ts index a0fc631..dd35503 100644 --- a/backend/src/v1/user/user.controller.ts +++ b/backend/src/v1/user/user.controller.ts @@ -149,6 +149,18 @@ export class UserController { return this.userService.getStreakInfo(+req.user.id); } + @Post('streak/freeze') + @ApiOperation({ summary: 'Use a streak freeze to protect the current week' }) + @ApiOkResponse({ + description: 'Updated streak info after consuming the freeze', + }) + useStreakFreeze(@Req() req: RequestWithUser, @Body() body: { date: string }) { + if (!req.user?.id) { + throw new UnauthorizedException('User not authenticated'); + } + return this.userService.useStreakFreeze(+req.user.id, body.date); + } + @Put('weekly-goal') @ApiOperation({ summary: 'Update weekly workout goal' }) @ApiOkResponse({ type: UserWithoutPasswordDto }) @@ -179,8 +191,13 @@ export class UserController { } @Get('export') - @ApiOperation({ summary: 'Export all user data (GDPR Art. 20 data portability)' }) - @ApiOkResponse({ description: 'JSON file containing all personal data for the authenticated user' }) + @ApiOperation({ + summary: 'Export all user data (GDPR Art. 20 data portability)', + }) + @ApiOkResponse({ + description: + 'JSON file containing all personal data for the authenticated user', + }) async exportData( @Req() req: RequestWithUser, @Res() res: Response, @@ -189,7 +206,10 @@ export class UserController { throw new UnauthorizedException('User not authenticated'); } const data = await this.userService.exportUserData(+req.user.id); - res.setHeader('Content-Disposition', 'attachment; filename="grindify-data-export.json"'); + res.setHeader( + 'Content-Disposition', + 'attachment; filename="grindify-data-export.json"', + ); res.setHeader('Content-Type', 'application/json'); res.send(JSON.stringify(data, null, 2)); } diff --git a/backend/src/v1/user/user.entity.ts b/backend/src/v1/user/user.entity.ts index aa93dd9..5e0ce83 100644 --- a/backend/src/v1/user/user.entity.ts +++ b/backend/src/v1/user/user.entity.ts @@ -65,6 +65,15 @@ export class User { @Column({ default: 0 }) currentWeekWorkouts: number; + @Column({ default: 1 }) + streakFreezes: number; + + @Column({ type: 'varchar', length: 10, nullable: true }) + streakFreezeUsedWeek: string | null; + + @Column({ default: 0 }) + completedGoalWeeksCount: number; + // Onboarding & Preferences @Column({ type: 'varchar', length: 20, nullable: true }) unitScale: string; // 'metric' or 'imperial' diff --git a/backend/src/v1/user/user.service.ts b/backend/src/v1/user/user.service.ts index e6041ac..e1d5b7e 100644 --- a/backend/src/v1/user/user.service.ts +++ b/backend/src/v1/user/user.service.ts @@ -259,14 +259,33 @@ export class UserService { lastWeekSunday, ); + // Get the ISO week key of the previous week for freeze checking + const prevWeekKey = this.getISOWeekKey(lastWeekMonday); + // Check if user met their goal in the previous week if (workoutDays < user.weeklyWorkoutGoal) { - // Didn't meet goal, reset streak to 0 - user.currentStreak = 0; + // Didn't meet goal — check if a freeze protects this week + if (user.streakFreezeUsedWeek === prevWeekKey) { + // Freeze consumed — streak survives + user.streakFreezeUsedWeek = null; + } else { + user.currentStreak = 0; + } } else if (weeksPassed > 1) { // More than one week passed (means they didn't workout at all in between) // Even if they met the goal in the last tracked week, they missed weeks in between user.currentStreak = 0; + // Clear any stale freeze + user.streakFreezeUsedWeek = null; + } else { + // Goal met for exactly last week — award a freeze if earned + user.completedGoalWeeksCount = (user.completedGoalWeeksCount || 0) + 1; + if ( + user.completedGoalWeeksCount % 2 === 0 && + (user.streakFreezes || 0) < 2 + ) { + user.streakFreezes = (user.streakFreezes || 0) + 1; + } } // If they met the goal and it's been exactly 1 week, streak continues @@ -275,6 +294,22 @@ export class UserService { } } + /** + * Returns the ISO week key for a given date, e.g. "2026-W18" + */ + private getISOWeekKey(date: Date): string { + const d = new Date( + Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()), + ); + const dayNum = d.getUTCDay() || 7; + d.setUTCDate(d.getUTCDate() + 4 - dayNum); + const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + const weekNo = Math.ceil( + ((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7, + ); + return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`; + } + /** * Count total sessions with either workout sessions or activity logs in a date range */ @@ -341,6 +376,48 @@ export class UserService { await this.userRepo.save(user); } + /** + * Use a streak freeze to protect the current week from a streak reset + */ + async useStreakFreeze( + userId: number, + date: string, + ): Promise<{ + currentStreak: number; + weeklyWorkoutGoal: number; + currentWeekWorkouts: number; + progressPercentage: number; + streakFreezes: number; + freezeUsedThisWeek: boolean; + streakFreezeUsedWeek: string | null; + }> { + const user = await this.userRepo.findOne({ where: { id: userId } }); + if (!user) throw new NotFoundException('User not found'); + + if ((user.streakFreezes || 0) <= 0) { + throw new BadRequestException('No streak freezes available'); + } + + const now = new Date(); + const currentWeekKey = this.getISOWeekKey(now); + const targetDate = new Date(date + 'T12:00:00'); + const targetWeekKey = this.getISOWeekKey(targetDate); + + if (targetWeekKey !== currentWeekKey) { + throw new BadRequestException('Can only freeze the current week'); + } + + if (user.streakFreezeUsedWeek === currentWeekKey) { + throw new BadRequestException('A freeze is already active this week'); + } + + user.streakFreezeUsedWeek = currentWeekKey; + user.streakFreezes = (user.streakFreezes || 0) - 1; + await this.userRepo.save(user); + + return this.getStreakInfo(userId); + } + /** * Get user's current streak information */ @@ -349,6 +426,9 @@ export class UserService { weeklyWorkoutGoal: number; currentWeekWorkouts: number; progressPercentage: number; + streakFreezes: number; + freezeUsedThisWeek: boolean; + streakFreezeUsedWeek: string | null; }> { const user = await this.userRepo.findOne({ where: { id: userId } }); if (!user) { @@ -390,11 +470,16 @@ export class UserService { ) : 0; + const currentWeekKey = this.getISOWeekKey(now); + return { currentStreak: user.currentStreak, weeklyWorkoutGoal: user.weeklyWorkoutGoal, currentWeekWorkouts: user.currentWeekWorkouts, progressPercentage: Math.round(progressPercentage), + streakFreezes: user.streakFreezes ?? 1, + freezeUsedThisWeek: user.streakFreezeUsedWeek === currentWeekKey, + streakFreezeUsedWeek: user.streakFreezeUsedWeek ?? null, }; } @@ -431,16 +516,35 @@ export class UserService { throw new NotFoundException('User not found'); } - const [exercises, workouts, sessions, activityLogs, weightLogs, progressPhotos, exerciseRecords] = - await Promise.all([ - this.exerciseRepo.find({ where: { createdBy: { id: userId } }, relations: ['media'] }), - this.workoutRepo.find({ where: { createdBy: { id: userId } }, relations: ['exercises'] }), - this.sessionRepo.find({ where: { user: { id: userId } }, relations: ['exercises', 'exercises.sets'] }), - this.activityLogRepo.find({ where: { user: { id: userId } } }), - this.weightLogRepo.find({ where: { user: { id: userId } } }), - this.progressPhotoRepo.find({ where: { user: { id: userId } } }), - this.exerciseRecordRepo.find({ where: { user: { id: userId } }, relations: ['exercise'] }), - ]); + const [ + exercises, + workouts, + sessions, + activityLogs, + weightLogs, + progressPhotos, + exerciseRecords, + ] = await Promise.all([ + this.exerciseRepo.find({ + where: { createdBy: { id: userId } }, + relations: ['media'], + }), + this.workoutRepo.find({ + where: { createdBy: { id: userId } }, + relations: ['exercises'], + }), + this.sessionRepo.find({ + where: { user: { id: userId } }, + relations: ['exercises', 'exercises.sets'], + }), + this.activityLogRepo.find({ where: { user: { id: userId } } }), + this.weightLogRepo.find({ where: { user: { id: userId } } }), + this.progressPhotoRepo.find({ where: { user: { id: userId } } }), + this.exerciseRecordRepo.find({ + where: { user: { id: userId } }, + relations: ['exercise'], + }), + ]); return { exportedAt: new Date().toISOString(), diff --git a/frontend/src/interfaces/User.interface.ts b/frontend/src/interfaces/User.interface.ts index 48d9f66..38af79f 100644 --- a/frontend/src/interfaces/User.interface.ts +++ b/frontend/src/interfaces/User.interface.ts @@ -44,6 +44,9 @@ export interface StreakInfo { weeklyWorkoutGoal: number currentWeekWorkouts: number progressPercentage: number + streakFreezes: number + freezeUsedThisWeek: boolean + streakFreezeUsedWeek: string | null } export interface CreateUser { diff --git a/frontend/src/locales/en.ts b/frontend/src/locales/en.ts index b5ae6e5..49d00ab 100644 --- a/frontend/src/locales/en.ts +++ b/frontend/src/locales/en.ts @@ -976,6 +976,16 @@ export default { activity: 'Activities', workoutAndActivity: 'Workout & Activity', scheduled: 'Scheduled', + frozenWeek: 'Frozen week', + freezeWeek: 'Freeze this week', + freezeActive: 'Streak freeze active', + freeze: 'Freeze', + freezes: 'Freezes', + active: 'Active', + freezesRemaining: '{count} freeze(s) remaining', + freezeDialogTitle: 'Use Streak Freeze?', + freezeDialogBody: + 'This will protect your streak for the current week even if you miss your weekly goal. You can still log workouts and they will count normally.', }, schedule: { registerWorkout: 'Register Workout', diff --git a/frontend/src/locales/sv.ts b/frontend/src/locales/sv.ts index 4093015..965a450 100644 --- a/frontend/src/locales/sv.ts +++ b/frontend/src/locales/sv.ts @@ -969,6 +969,16 @@ export default { activity: 'Aktivitet', workoutAndActivity: 'Pass & Aktivitet', scheduled: 'Schemalagd', + frozenWeek: 'Fryst vecka', + freezeWeek: 'Frys denna vecka', + freezeActive: 'Streak-frys aktiv', + freeze: 'Frys', + freezes: 'Frysningar', + active: 'Aktiv', + freezesRemaining: '{count} frys(ar) kvar', + freezeDialogTitle: 'Använd streak-frys?', + freezeDialogBody: + 'Detta skyddar din streak för nuvarande vecka även om du missar ditt veckliga mål. Du kan fortfarande logga träning och det räknas som vanligt.', }, schedule: { registerWorkout: 'Registrera träning', diff --git a/frontend/src/pages/Calendar.vue b/frontend/src/pages/Calendar.vue index c408b8c..d11f9f9 100644 --- a/frontend/src/pages/Calendar.vue +++ b/frontend/src/pages/Calendar.vue @@ -23,24 +23,41 @@ -
- - mdi-fire - -
-

{{ $t('calendar.workoutsThisMonth') }}

-

{{ workoutsThisMonth }} {{ $t('calendar.workout') }}

+
+ +
+ mdi-dumbbell + {{ workoutSessionsThisMonth }} + {{ + $t('calendar.workout') + }}
-
-
-

{{ $t('calendar.currentStreak') }}

-
-

- {{ streakInfo?.currentStreak || 0 }} {{ $t('calendar.days') }} -

+ + + + +
+ mdi-run + {{ activitiesThisMonth }} + {{ + $t('calendar.activity') + }} +
+ + + + +
+ mdi-snowflake + {{ + streakInfo?.streakFreezes ?? 1 + }} + {{ + $t('calendar.freezes') + }}
@@ -83,6 +100,7 @@ 'other-month': !day.isCurrentMonth, 'is-today': day.isToday, 'is-selected': day.date === selectedDate, + 'is-frozen-week': day.isFrozenWeek, }" @click="selectDay(day)" > @@ -94,6 +112,7 @@ 'badge-activity': day.trainingType === 'activity', 'badge-both': day.trainingType === 'both', 'badge-scheduled': day.hasScheduledOnly, + 'badge-frozen': day.isFrozenWeek && !day.trainingType && !day.hasScheduledOnly, }" > {{ day.dayNumber }} @@ -122,11 +141,61 @@
{{ $t('calendar.scheduled') }}
+
+
+ {{ $t('calendar.frozenWeek') }} +
+ + +
+
+ + mdi-snowflake + +
+

+ {{ + streakInfo?.freezeUsedThisWeek + ? $t('calendar.freezeActive') + : $t('calendar.freezeWeek') + }} +

+

+ {{ $t('calendar.freezesRemaining', { count: streakInfo?.streakFreezes ?? 1 }) }} +

+
+
+ + {{ $t('calendar.freeze') }} + + + {{ $t('calendar.active') }} + +
+
+

{{ selectedDateLabel }}

@@ -314,6 +383,35 @@ @started="onScheduleStarted" @log-past="onLogPastFromSchedule" /> + + + + + + mdi-snowflake + {{ $t('calendar.freezeDialogTitle') }} + + +

{{ $t('calendar.freezeDialogBody') }}

+

+ {{ $t('calendar.freezesRemaining', { count: streakInfo?.streakFreezes ?? 1 }) }} +

+
+ + {{ + $t('common.cancel') + }} + + {{ $t('calendar.freeze') }} + + +
+
@@ -323,7 +421,7 @@ import { useWorkoutSessionStore } from '@/stores/workoutSession.store' import { useActivityStore } from '@/stores/activity.store' import { useScheduledSessionStore } from '@/stores/scheduledSession.store' import { useRouter } from 'vue-router' -import { getStreakInfo } from '@/services/user.service' +import { getStreakInfo, useStreakFreeze } from '@/services/user.service' import { startWorkoutSession } from '@/services/workoutSession.service' import type { WorkoutSession } from '@/interfaces/workoutSession.interface' import type { ActivityLog } from '@/interfaces/Activity.interface' @@ -350,6 +448,8 @@ const isScheduleDialogOpen = ref(false) const isAddPastDialogOpen = ref(false) const isBottomSheetOpen = ref(false) const selectedScheduledSession = ref(null) +const isFreezeDialogOpen = ref(false) +const isFreezeLoading = ref(false) // Pre-selection state for AddPastSessionDialog (from scheduled session) const pastSessionPreselectedType = ref<'workout' | 'activity' | undefined>(undefined) @@ -376,6 +476,7 @@ interface CalendarDay { hasCompletedTraining: boolean trainingType: 'workout' | 'activity' | 'both' | null hasScheduledOnly: boolean + isFrozenWeek: boolean events: CalendarEvent[] } @@ -405,6 +506,49 @@ const isSelectedDateFuture = computed(() => selectedDate.value > todayStr.value) const isSelectedDateFutureOrToday = computed(() => selectedDate.value >= todayStr.value) +/** Whether the selected date falls in the current ISO week */ +const isSelectedDateInCurrentWeek = computed(() => { + const today = new Date() + const getMondayOfWeek = (date: Date): Date => { + const d = new Date(date) + const day = d.getDay() + const diff = day === 0 ? -6 : 1 - day + d.setDate(d.getDate() + diff) + d.setHours(0, 0, 0, 0) + return d + } + const monday = getMondayOfWeek(today) + const sunday = new Date(monday) + sunday.setDate(monday.getDate() + 6) + sunday.setHours(23, 59, 59, 999) + const selected = new Date(selectedDate.value + 'T12:00:00') + return selected >= monday && selected <= sunday +}) + +/** Set of date strings belonging to the currently frozen ISO week */ +const frozenWeekDates = computed>(() => { + const key = streakInfo.value?.streakFreezeUsedWeek + if (!key) return new Set() + const [yearStr, weekStr] = key.split('-W') + const year = parseInt(yearStr) + const week = parseInt(weekStr) + // Get Monday of ISO week 1 for that year + const jan4 = new Date(year, 0, 4) + const dayOfWeek = jan4.getDay() || 7 + const week1Monday = new Date(jan4) + week1Monday.setDate(jan4.getDate() - dayOfWeek + 1) + // Advance to the target week's Monday + const monday = new Date(week1Monday) + monday.setDate(week1Monday.getDate() + (week - 1) * 7) + const dates = new Set() + for (let i = 0; i < 7; i++) { + const d = new Date(monday) + d.setDate(monday.getDate() + i) + dates.add(toLocalDateString(d)) + } + return dates +}) + const selectedDateLabel = computed(() => { if (isSelectedDateToday.value) return t('calendar.today') @@ -416,12 +560,11 @@ const selectedDateLabel = computed(() => { }) }) -// Workouts this month (dynamic) -const workoutsThisMonth = computed(() => { +// Workouts this month (sessions only) +const workoutSessionsThisMonth = computed(() => { const year = currentDate.value.getFullYear() const month = currentDate.value.getMonth() let count = 0 - if (workoutSessionStore.workoutSessions) { ;(workoutSessionStore.workoutSessions as WorkoutSession[]).forEach(session => { if (session.status === 'finished' && session.endedAt) { @@ -430,14 +573,20 @@ const workoutsThisMonth = computed(() => { } }) } + return count +}) +// Activities this month (activity logs only) +const activitiesThisMonth = computed(() => { + const year = currentDate.value.getFullYear() + const month = currentDate.value.getMonth() + let count = 0 if (activityStore.activityLogs) { ;(activityStore.activityLogs as ActivityLog[]).forEach(log => { const d = new Date(log.date) if (d.getFullYear() === year && d.getMonth() === month) count++ }) } - return count }) @@ -573,6 +722,7 @@ const calendarDays = computed(() => { hasCompletedTraining: hasCompletedTrainingEvents(events), trainingType: getTrainingType(events), hasScheduledOnly: hasScheduledOnlyEvents(events), + isFrozenWeek: frozenWeekDates.value.has(dateStr), events, }) } @@ -591,6 +741,7 @@ const calendarDays = computed(() => { hasCompletedTraining: hasCompletedTrainingEvents(events), trainingType: getTrainingType(events), hasScheduledOnly: hasScheduledOnlyEvents(events), + isFrozenWeek: frozenWeekDates.value.has(dateStr), events, }) } @@ -609,6 +760,7 @@ const calendarDays = computed(() => { hasCompletedTraining: hasCompletedTrainingEvents(events), trainingType: getTrainingType(events), hasScheduledOnly: hasScheduledOnlyEvents(events), + isFrozenWeek: frozenWeekDates.value.has(dateStr), events, }) } @@ -734,6 +886,19 @@ function openCompletedSession(event: CalendarEvent) { if (event.type === 'scheduled') return router.push(`/session-history/${event.type}/${event.sessionId}`) } + +async function freezeWeek() { + isFreezeLoading.value = true + try { + const updated = await useStreakFreeze(selectedDate.value) + streakInfo.value = updated + isFreezeDialogOpen.value = false + } catch (error) { + console.error('Failed to use streak freeze:', error) + } finally { + isFreezeLoading.value = false + } +}