@dgpholdings/greatoak-shared 1.2.36 → 1.2.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -116,3 +116,34 @@ shared/
|
|
|
116
116
|
- **No framework code** — this package must stay framework-agnostic (no React, no NestJS decorators)
|
|
117
117
|
- **Strict types** — no `any`. Use generics, discriminated unions, or `unknown`
|
|
118
118
|
- **Scoring is the business core** — the scoring engine drives the muscle fatigue diagram and progress charts. Read [docs/scoring.md](docs/scoring.md) before touching it
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
## Renewing token
|
|
122
|
+
|
|
123
|
+
1. Login using the token (recommended way)
|
|
124
|
+
|
|
125
|
+
In your Git Bash, run:
|
|
126
|
+
|
|
127
|
+
`npm login`
|
|
128
|
+
|
|
129
|
+
It will prompt:
|
|
130
|
+
|
|
131
|
+
Username:
|
|
132
|
+
Password:
|
|
133
|
+
Email:
|
|
134
|
+
|
|
135
|
+
Use this:
|
|
136
|
+
|
|
137
|
+
Username → your npm username
|
|
138
|
+
|
|
139
|
+
Password → paste the granular token (NOT your npm password)
|
|
140
|
+
|
|
141
|
+
Email → your npm email
|
|
142
|
+
|
|
143
|
+
After that, npm stores the token in:
|
|
144
|
+
|
|
145
|
+
`shared\.npmrc`
|
|
146
|
+
|
|
147
|
+
Then publishing works normally:
|
|
148
|
+
|
|
149
|
+
`npm publish`
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { TTemplateExercise } from "./commonTypes";
|
|
2
|
-
|
|
2
|
+
import { TGender } from "./TApiUser";
|
|
3
|
+
export type TProPlanTags = "medical" | "free-hand" | "all-gym-equip" | "light-gym-equip" | "bands-free-hand" | "editors-choise";
|
|
4
|
+
export type TProPlanDayId = "day-1" | "day-2" | "day-3" | "day-4" | "day-5" | "day-6" | "day-7";
|
|
3
5
|
export type TProPlan = {
|
|
4
6
|
name: {
|
|
5
7
|
default: string;
|
|
@@ -18,16 +20,14 @@ export type TProPlan = {
|
|
|
18
20
|
fr?: string;
|
|
19
21
|
};
|
|
20
22
|
planId: string;
|
|
21
|
-
|
|
22
|
-
gender: "male" | "female" | "all";
|
|
23
|
-
slug: string;
|
|
23
|
+
gender: TGender;
|
|
24
24
|
level: "beginner" | "intermediate" | "advanced" | "beginner-intermediate" | "intermediate-advanced" | "all";
|
|
25
25
|
tags: TProPlanTags[];
|
|
26
26
|
status: "active" | "achieved" | "draft";
|
|
27
27
|
version: number;
|
|
28
28
|
versionNote?: string;
|
|
29
29
|
days: {
|
|
30
|
-
dayId:
|
|
30
|
+
dayId: TProPlanDayId;
|
|
31
31
|
name: {
|
|
32
32
|
default: string;
|
|
33
33
|
en?: string;
|
|
@@ -36,7 +36,7 @@ export type TProPlan = {
|
|
|
36
36
|
es?: string;
|
|
37
37
|
fr?: string;
|
|
38
38
|
};
|
|
39
|
-
|
|
39
|
+
exerciseOrder: TTemplateExercise[][];
|
|
40
40
|
estimatedDuration: number;
|
|
41
41
|
planThumbnailImageUrl?: string;
|
|
42
42
|
}[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TProPlan, TUserMetric, TExercise } from "../../types";
|
|
1
|
+
import { TProPlan, TUserMetric, TExercise, TTemplateExercise } from "../../types";
|
|
2
2
|
/**
|
|
3
3
|
* Calculates the BMI (Body Mass Index).
|
|
4
4
|
*/
|
|
@@ -7,3 +7,12 @@ export declare const calculateBMI: (weightKg: number, heightCm: number) => numbe
|
|
|
7
7
|
* The Adoption Engine: Safely scales a Master Pro Plan to fit an individual user's body type.
|
|
8
8
|
*/
|
|
9
9
|
export declare const scaleProPlan: (masterPlan: TProPlan, userMetric: TUserMetric, exercisesDictionary: Record<string, TExercise>) => TProPlan;
|
|
10
|
+
/**
|
|
11
|
+
* Calculates the estimated duration (in seconds) for an exercise based on timing guardrails.
|
|
12
|
+
*/
|
|
13
|
+
export declare const calculateExerciseDurationSecs: (templateExercise: TTemplateExercise, exerciseMeta: TExercise | undefined) => number;
|
|
14
|
+
/**
|
|
15
|
+
* Calculates the total estimated duration (in minutes) for a day's plan.
|
|
16
|
+
* It averages the duration of alternative exercises within a single slot ("OR" pattern).
|
|
17
|
+
*/
|
|
18
|
+
export declare const calculateDayPlanDuration: (exerciseOrder: TTemplateExercise[][], exercisesDictionary: Record<string, TExercise>) => number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.scaleProPlan = exports.calculateBMI = void 0;
|
|
3
|
+
exports.calculateDayPlanDuration = exports.calculateExerciseDurationSecs = exports.scaleProPlan = exports.calculateBMI = void 0;
|
|
4
4
|
const NO_THRESHOLD = Infinity;
|
|
5
5
|
/**
|
|
6
6
|
* Calculates the BMI (Body Mass Index).
|
|
@@ -75,65 +75,149 @@ const scaleProPlan = (masterPlan, userMetric, exercisesDictionary) => {
|
|
|
75
75
|
const { weightKg, heightCm, gender, fitnessLevel } = userMetric;
|
|
76
76
|
const bmi = weightKg && heightCm ? (0, exports.calculateBMI)(weightKg, heightCm) : 0;
|
|
77
77
|
tailoredPlan.days.forEach((day) => {
|
|
78
|
-
day.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
78
|
+
day.exerciseOrder = day.exerciseOrder.map((slot) => {
|
|
79
|
+
return slot.map((templateExercise) => {
|
|
80
|
+
const exerciseMeta = exercisesDictionary[templateExercise.exerciseId];
|
|
81
|
+
// If we can't find metadata, return as-is
|
|
82
|
+
if (!exerciseMeta)
|
|
83
|
+
return templateExercise;
|
|
84
|
+
let isModified = false;
|
|
85
|
+
let newExerciseId = templateExercise.exerciseId;
|
|
86
|
+
let newRecords = [...templateExercise.initialRecords];
|
|
87
|
+
// 1. Check Safety Triggers (BMI & Bodyweight Dependency)
|
|
88
|
+
if (bmi > 0 &&
|
|
89
|
+
(exerciseMeta.bodyweightDependency === "high" ||
|
|
90
|
+
exerciseMeta.bodyweightDependency === "medium") &&
|
|
91
|
+
exerciseMeta.bmiThresholds) {
|
|
92
|
+
const obeseTrigger = getAdjustedBMIThreshold(exerciseMeta.bmiThresholds.obeseTrigger, fitnessLevel);
|
|
93
|
+
const overweightTrigger = getAdjustedBMIThreshold(exerciseMeta.bmiThresholds.overweightTrigger, fitnessLevel);
|
|
94
|
+
if (bmi >= obeseTrigger) {
|
|
95
|
+
// Tier 1: Hard swap to regression (if available) AND scale down volume
|
|
96
|
+
newRecords = newRecords.map(applyFallbackScalingToRecord);
|
|
97
|
+
isModified = true;
|
|
98
|
+
if (exerciseMeta.regressionExerciseId &&
|
|
99
|
+
exercisesDictionary[exerciseMeta.regressionExerciseId]) {
|
|
100
|
+
newExerciseId = exerciseMeta.regressionExerciseId;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else if (bmi >= overweightTrigger) {
|
|
104
|
+
// Tier 2: Softer response. Keep the exercise, but scale down the volume/duration.
|
|
105
|
+
newRecords = newRecords.map(applyFallbackScalingToRecord);
|
|
106
|
+
isModified = true;
|
|
98
107
|
}
|
|
99
108
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
: exerciseMeta.weightMultiplier.default;
|
|
113
|
-
if (multiplier && multiplier > 0) {
|
|
114
|
-
// Round to nearest 2.5kg to match real-world gym equipment
|
|
115
|
-
const suggestedLoadKg = roundToNearest(weightKg * multiplier, 2.5);
|
|
116
|
-
newRecords = newRecords.map((record) => {
|
|
117
|
-
if (record.type === "weight-reps") {
|
|
118
|
-
isModified = true;
|
|
119
|
-
return Object.assign(Object.assign({}, record), { kg: suggestedLoadKg.toString() });
|
|
120
|
-
}
|
|
121
|
-
// Guard against applying load to a falsy "0" aux weight
|
|
122
|
-
if (record.type === "reps-only" || record.type === "duration" || record.type === "cardio-free") {
|
|
123
|
-
const currentAux = parseFloat(record.auxWeightKg || "0");
|
|
124
|
-
if (!isNaN(currentAux) && currentAux > 0) {
|
|
109
|
+
// 2. Check Load Multipliers
|
|
110
|
+
if (weightKg && exerciseMeta.weightMultiplier && newRecords.length > 0) {
|
|
111
|
+
const multiplier = gender === "male"
|
|
112
|
+
? exerciseMeta.weightMultiplier.male
|
|
113
|
+
: gender === "female"
|
|
114
|
+
? exerciseMeta.weightMultiplier.female
|
|
115
|
+
: exerciseMeta.weightMultiplier.default;
|
|
116
|
+
if (multiplier && multiplier > 0) {
|
|
117
|
+
// Round to nearest 2.5kg to match real-world gym equipment
|
|
118
|
+
const suggestedLoadKg = roundToNearest(weightKg * multiplier, 2.5);
|
|
119
|
+
newRecords = newRecords.map((record) => {
|
|
120
|
+
if (record.type === "weight-reps") {
|
|
125
121
|
isModified = true;
|
|
126
|
-
return Object.assign(Object.assign({}, record), {
|
|
122
|
+
return Object.assign(Object.assign({}, record), { kg: suggestedLoadKg.toString() });
|
|
127
123
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
124
|
+
// Guard against applying load to a falsy "0" aux weight
|
|
125
|
+
if (record.type === "reps-only" ||
|
|
126
|
+
record.type === "duration" ||
|
|
127
|
+
record.type === "cardio-free") {
|
|
128
|
+
const currentAux = parseFloat(record.auxWeightKg || "0");
|
|
129
|
+
if (!isNaN(currentAux) && currentAux > 0) {
|
|
130
|
+
isModified = true;
|
|
131
|
+
return Object.assign(Object.assign({}, record), { auxWeightKg: suggestedLoadKg.toString() });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return record;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
131
137
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
// Return the tailored template exercise
|
|
139
|
+
return Object.assign(Object.assign({}, templateExercise), { exerciseId: newExerciseId, initialRecords: newRecords, isAutoScaled: isModified || undefined });
|
|
140
|
+
});
|
|
135
141
|
});
|
|
136
142
|
});
|
|
137
143
|
return tailoredPlan;
|
|
138
144
|
};
|
|
139
145
|
exports.scaleProPlan = scaleProPlan;
|
|
146
|
+
/**
|
|
147
|
+
* Calculates the estimated duration (in seconds) for an exercise based on timing guardrails.
|
|
148
|
+
*/
|
|
149
|
+
const calculateExerciseDurationSecs = (templateExercise, exerciseMeta) => {
|
|
150
|
+
if (!exerciseMeta || !exerciseMeta.timingGuardrails)
|
|
151
|
+
return 0;
|
|
152
|
+
let guardrails = exerciseMeta.timingGuardrails;
|
|
153
|
+
// Defensive parse in case it's a JSON string from DB
|
|
154
|
+
if (typeof guardrails === "string") {
|
|
155
|
+
try {
|
|
156
|
+
guardrails = JSON.parse(guardrails);
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
let totalSecs = guardrails.setupTypicalSecs || 0;
|
|
163
|
+
const records = templateExercise.initialRecords || [];
|
|
164
|
+
records.forEach((record, index) => {
|
|
165
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
166
|
+
let workSecs = 0;
|
|
167
|
+
if (record.type === "reps-only" || record.type === "weight-reps") {
|
|
168
|
+
const repsStr = "reps" in record ? record.reps : "0";
|
|
169
|
+
const reps = parseInt(repsStr, 10) || 0;
|
|
170
|
+
let singleRepSecs = 2.5; // fallback
|
|
171
|
+
if (guardrails.type === "weight-reps" || guardrails.type === "reps-only") {
|
|
172
|
+
singleRepSecs = (_b = (_a = guardrails.singleRep) === null || _a === void 0 ? void 0 : _a.typical) !== null && _b !== void 0 ? _b : 2.5;
|
|
173
|
+
}
|
|
174
|
+
workSecs = reps * singleRepSecs;
|
|
175
|
+
}
|
|
176
|
+
else if (record.type === "duration" ||
|
|
177
|
+
record.type === "cardio-machine" ||
|
|
178
|
+
record.type === "cardio-free") {
|
|
179
|
+
const durationStr = "durationMmSs" in record ? record.durationMmSs : "00:00";
|
|
180
|
+
const [mm, ss] = (durationStr || "00:00").split(":").map((v) => parseInt(v, 10) || 0);
|
|
181
|
+
workSecs = (mm * 60) + ss;
|
|
182
|
+
if (workSecs === 0 && guardrails.type === "duration") {
|
|
183
|
+
workSecs = (_d = (_c = guardrails.setDuration) === null || _c === void 0 ? void 0 : _c.typical) !== null && _d !== void 0 ? _d : 60;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
totalSecs += workSecs;
|
|
187
|
+
// Add rest time if not the last set
|
|
188
|
+
if (index < records.length - 1) {
|
|
189
|
+
const restTime = (_h = (_f = (_e = record.restDurationSecs) !== null && _e !== void 0 ? _e : templateExercise.restTimeSecs) !== null && _f !== void 0 ? _f : (_g = guardrails.restPeriods) === null || _g === void 0 ? void 0 : _g.typical) !== null && _h !== void 0 ? _h : 60;
|
|
190
|
+
totalSecs += restTime;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
return totalSecs;
|
|
194
|
+
};
|
|
195
|
+
exports.calculateExerciseDurationSecs = calculateExerciseDurationSecs;
|
|
196
|
+
/**
|
|
197
|
+
* Calculates the total estimated duration (in minutes) for a day's plan.
|
|
198
|
+
* It averages the duration of alternative exercises within a single slot ("OR" pattern).
|
|
199
|
+
*/
|
|
200
|
+
const calculateDayPlanDuration = (exerciseOrder, exercisesDictionary) => {
|
|
201
|
+
if (!exerciseOrder || !Array.isArray(exerciseOrder))
|
|
202
|
+
return 0;
|
|
203
|
+
let totalDaySecs = 0;
|
|
204
|
+
exerciseOrder.forEach((slot) => {
|
|
205
|
+
if (!Array.isArray(slot) || slot.length === 0)
|
|
206
|
+
return;
|
|
207
|
+
let slotTotalSecs = 0;
|
|
208
|
+
let validExercisesCount = 0;
|
|
209
|
+
slot.forEach((templateExercise) => {
|
|
210
|
+
const exerciseMeta = exercisesDictionary[templateExercise.exerciseId];
|
|
211
|
+
if (exerciseMeta) {
|
|
212
|
+
slotTotalSecs += (0, exports.calculateExerciseDurationSecs)(templateExercise, exerciseMeta);
|
|
213
|
+
validExercisesCount++;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
if (validExercisesCount > 0) {
|
|
217
|
+
totalDaySecs += slotTotalSecs / validExercisesCount;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
// Return duration in minutes (rounded up to nearest minute)
|
|
221
|
+
return Math.ceil(totalDaySecs / 60);
|
|
222
|
+
};
|
|
223
|
+
exports.calculateDayPlanDuration = calculateDayPlanDuration;
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -8,4 +8,4 @@ export { generatePlanCode } from "./planCode.util";
|
|
|
8
8
|
export { maskEmail, isAnonymousEmail, isEmail } from "./email.utils";
|
|
9
9
|
export { NOOP } from "./noop.utils";
|
|
10
10
|
export { calculateExerciseScoreV2, calculateTotalVolume } from "./scoring";
|
|
11
|
-
export { scaleProPlan, calculateBMI } from "./adoptionEngine/scaleProPlan.util";
|
|
11
|
+
export { scaleProPlan, calculateBMI, calculateDayPlanDuration, calculateExerciseDurationSecs } from "./adoptionEngine/scaleProPlan.util";
|
package/dist/utils/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.calculateBMI = exports.scaleProPlan = exports.calculateTotalVolume = exports.calculateExerciseScoreV2 = exports.NOOP = exports.isEmail = exports.isAnonymousEmail = exports.maskEmail = exports.generatePlanCode = exports.toError = exports.slugifyText = exports.isDefinedNumber = exports.isDefined = exports.countryToCurrencyCode = exports.getDaysAndHoursDifference = exports.isUserAllowedToUpdate = exports.mmssToSecs = exports.toNumber = void 0;
|
|
3
|
+
exports.calculateExerciseDurationSecs = exports.calculateDayPlanDuration = exports.calculateBMI = exports.scaleProPlan = exports.calculateTotalVolume = exports.calculateExerciseScoreV2 = exports.NOOP = exports.isEmail = exports.isAnonymousEmail = exports.maskEmail = exports.generatePlanCode = exports.toError = exports.slugifyText = exports.isDefinedNumber = exports.isDefined = exports.countryToCurrencyCode = exports.getDaysAndHoursDifference = exports.isUserAllowedToUpdate = exports.mmssToSecs = exports.toNumber = void 0;
|
|
4
4
|
var number_util_1 = require("./number.util");
|
|
5
5
|
Object.defineProperty(exports, "toNumber", { enumerable: true, get: function () { return number_util_1.toNumber; } });
|
|
6
6
|
var time_util_1 = require("./time.util");
|
|
@@ -30,3 +30,5 @@ Object.defineProperty(exports, "calculateTotalVolume", { enumerable: true, get:
|
|
|
30
30
|
var scaleProPlan_util_1 = require("./adoptionEngine/scaleProPlan.util");
|
|
31
31
|
Object.defineProperty(exports, "scaleProPlan", { enumerable: true, get: function () { return scaleProPlan_util_1.scaleProPlan; } });
|
|
32
32
|
Object.defineProperty(exports, "calculateBMI", { enumerable: true, get: function () { return scaleProPlan_util_1.calculateBMI; } });
|
|
33
|
+
Object.defineProperty(exports, "calculateDayPlanDuration", { enumerable: true, get: function () { return scaleProPlan_util_1.calculateDayPlanDuration; } });
|
|
34
|
+
Object.defineProperty(exports, "calculateExerciseDurationSecs", { enumerable: true, get: function () { return scaleProPlan_util_1.calculateExerciseDurationSecs; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dgpholdings/greatoak-shared",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.44",
|
|
4
4
|
"description": "Shared TypeScript types and utilities for @dgpholdings projects",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"repository": {
|
|
17
17
|
"type": "git",
|
|
18
|
-
"url": "https://
|
|
18
|
+
"url": "https://gitlab.com/greatoak/shared.git"
|
|
19
19
|
},
|
|
20
20
|
"author": "Siddhartha Chowdhury",
|
|
21
21
|
"license": "MIT",
|