@jjlmoya/utils-health 1.18.0 → 1.20.0
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/package.json +1 -1
- package/src/category/index.ts +3 -1
- package/src/data.ts +1 -0
- package/src/entries.ts +5 -1
- package/src/index.ts +3 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/dailyProteinCalculator/bibliography.astro +14 -0
- package/src/tool/dailyProteinCalculator/bibliography.ts +28 -0
- package/src/tool/dailyProteinCalculator/component.astro +282 -0
- package/src/tool/dailyProteinCalculator/daily-protein-calculator.css +305 -0
- package/src/tool/dailyProteinCalculator/entry.ts +27 -0
- package/src/tool/dailyProteinCalculator/i18n/de.ts +353 -0
- package/src/tool/dailyProteinCalculator/i18n/en.ts +353 -0
- package/src/tool/dailyProteinCalculator/i18n/es.ts +353 -0
- package/src/tool/dailyProteinCalculator/i18n/fr.ts +353 -0
- package/src/tool/dailyProteinCalculator/i18n/id.ts +353 -0
- package/src/tool/dailyProteinCalculator/i18n/it.ts +353 -0
- package/src/tool/dailyProteinCalculator/i18n/ja.ts +353 -0
- package/src/tool/dailyProteinCalculator/i18n/ko.ts +353 -0
- package/src/tool/dailyProteinCalculator/i18n/nl.ts +353 -0
- package/src/tool/dailyProteinCalculator/i18n/pl.ts +353 -0
- package/src/tool/dailyProteinCalculator/i18n/pt.ts +353 -0
- package/src/tool/dailyProteinCalculator/i18n/ru.ts +353 -0
- package/src/tool/dailyProteinCalculator/i18n/sv.ts +358 -0
- package/src/tool/dailyProteinCalculator/i18n/tr.ts +87 -0
- package/src/tool/dailyProteinCalculator/i18n/zh.ts +328 -0
- package/src/tool/dailyProteinCalculator/index.ts +12 -0
- package/src/tool/dailyProteinCalculator/logic.ts +65 -0
- package/src/tool/dailyProteinCalculator/seo.astro +15 -0
- package/src/tool/dailyProteinCalculator/ui.ts +40 -0
- package/src/tool/metabolicAgeCalculator/bibliography.astro +14 -0
- package/src/tool/metabolicAgeCalculator/bibliography.ts +28 -0
- package/src/tool/metabolicAgeCalculator/component.astro +242 -0
- package/src/tool/metabolicAgeCalculator/entry.ts +27 -0
- package/src/tool/metabolicAgeCalculator/i18n/de.ts +319 -0
- package/src/tool/metabolicAgeCalculator/i18n/en.ts +319 -0
- package/src/tool/metabolicAgeCalculator/i18n/es.ts +319 -0
- package/src/tool/metabolicAgeCalculator/i18n/fr.ts +319 -0
- package/src/tool/metabolicAgeCalculator/i18n/id.ts +319 -0
- package/src/tool/metabolicAgeCalculator/i18n/it.ts +319 -0
- package/src/tool/metabolicAgeCalculator/i18n/ja.ts +319 -0
- package/src/tool/metabolicAgeCalculator/i18n/ko.ts +319 -0
- package/src/tool/metabolicAgeCalculator/i18n/nl.ts +319 -0
- package/src/tool/metabolicAgeCalculator/i18n/pl.ts +319 -0
- package/src/tool/metabolicAgeCalculator/i18n/pt.ts +319 -0
- package/src/tool/metabolicAgeCalculator/i18n/ru.ts +319 -0
- package/src/tool/metabolicAgeCalculator/i18n/sv.ts +319 -0
- package/src/tool/metabolicAgeCalculator/i18n/tr.ts +319 -0
- package/src/tool/metabolicAgeCalculator/i18n/zh.ts +319 -0
- package/src/tool/metabolicAgeCalculator/index.ts +12 -0
- package/src/tool/metabolicAgeCalculator/logic.ts +120 -0
- package/src/tool/metabolicAgeCalculator/metabolic-age-calculator.css +393 -0
- package/src/tool/metabolicAgeCalculator/seo.astro +15 -0
- package/src/tool/metabolicAgeCalculator/ui.ts +49 -0
- package/src/tools.ts +3 -1
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { daltonismSimulator } from '../tool/daltonismSimulator/entry';
|
|
|
5
5
|
import { tinnitusReliever } from '../tool/tinnitusReliever/entry';
|
|
6
6
|
import { breathingVisualizer } from '../tool/breathingVisualizer/entry';
|
|
7
7
|
import { caffeineTracker } from '../tool/caffeineTracker/entry';
|
|
8
|
+
import { dailyProteinCalculator } from '../tool/dailyProteinCalculator/entry';
|
|
8
9
|
import { waterPurifier } from '../tool/waterPurifier/entry';
|
|
9
10
|
import { digestionStopwatch } from '../tool/digestionStopwatch/entry';
|
|
10
11
|
import { binauralTuner } from '../tool/binauralTuner/entry';
|
|
@@ -12,10 +13,11 @@ import { pelliRobsonTest } from '../tool/pelliRobsonTest/entry';
|
|
|
12
13
|
import { peripheralVisionTrainer } from '../tool/peripheralVisionTrainer/entry';
|
|
13
14
|
import { epworthSleepinessScale } from '../tool/epworthSleepinessScale/entry';
|
|
14
15
|
import { leanBodyMassCalculator } from '../tool/leanBodyMassCalculator/entry';
|
|
16
|
+
import { metabolicAgeCalculator } from '../tool/metabolicAgeCalculator/entry';
|
|
15
17
|
|
|
16
18
|
export const healthCategory: HealthCategoryEntry = {
|
|
17
19
|
icon: 'mdi:heart-pulse',
|
|
18
|
-
tools: [bmiCalculator, bodyAdiposityIndexCalculator, leanBodyMassCalculator, daltonismSimulator, tinnitusReliever, breathingVisualizer, caffeineTracker, waterPurifier, digestionStopwatch, binauralTuner, pelliRobsonTest, peripheralVisionTrainer, epworthSleepinessScale],
|
|
20
|
+
tools: [bmiCalculator, bodyAdiposityIndexCalculator, leanBodyMassCalculator, metabolicAgeCalculator, dailyProteinCalculator, daltonismSimulator, tinnitusReliever, breathingVisualizer, caffeineTracker, waterPurifier, digestionStopwatch, binauralTuner, pelliRobsonTest, peripheralVisionTrainer, epworthSleepinessScale],
|
|
19
21
|
i18n: {
|
|
20
22
|
en: () => import('./i18n/en').then((m) => m.content),
|
|
21
23
|
es: () => import('./i18n/es').then((m) => m.content),
|
package/src/data.ts
CHANGED
|
@@ -15,6 +15,7 @@ export { bloodUnitConverter } from './tool/bloodUnitConverter';
|
|
|
15
15
|
export { readingDistanceCalculator } from './tool/readingDistanceCalculator';
|
|
16
16
|
export { hydrationCalculator } from './tool/hydrationCalculator';
|
|
17
17
|
export { leanBodyMassCalculator } from './tool/leanBodyMassCalculator';
|
|
18
|
+
export { metabolicAgeCalculator } from './tool/metabolicAgeCalculator';
|
|
18
19
|
|
|
19
20
|
export { healthCategory } from './category';
|
|
20
21
|
|
package/src/entries.ts
CHANGED
|
@@ -4,11 +4,13 @@ export { bmiCalculator } from './tool/bmiCalculator/entry';
|
|
|
4
4
|
export { bodyAdiposityIndexCalculator } from './tool/bodyAdiposityIndexCalculator/entry';
|
|
5
5
|
export { breathingVisualizer } from './tool/breathingVisualizer/entry';
|
|
6
6
|
export { caffeineTracker } from './tool/caffeineTracker/entry';
|
|
7
|
+
export { dailyProteinCalculator } from './tool/dailyProteinCalculator/entry';
|
|
7
8
|
export { daltonismSimulator } from './tool/daltonismSimulator/entry';
|
|
8
9
|
export { digestionStopwatch } from './tool/digestionStopwatch/entry';
|
|
9
10
|
export { epworthSleepinessScale } from './tool/epworthSleepinessScale/entry';
|
|
10
11
|
export { hydrationCalculator } from './tool/hydrationCalculator/entry';
|
|
11
12
|
export { leanBodyMassCalculator } from './tool/leanBodyMassCalculator/entry';
|
|
13
|
+
export { metabolicAgeCalculator } from './tool/metabolicAgeCalculator/entry';
|
|
12
14
|
export { pelliRobsonTest } from './tool/pelliRobsonTest/entry';
|
|
13
15
|
export { peripheralVisionTrainer } from './tool/peripheralVisionTrainer/entry';
|
|
14
16
|
export { readingDistanceCalculator } from './tool/readingDistanceCalculator/entry';
|
|
@@ -23,11 +25,13 @@ import { bmiCalculator } from './tool/bmiCalculator/entry';
|
|
|
23
25
|
import { bodyAdiposityIndexCalculator } from './tool/bodyAdiposityIndexCalculator/entry';
|
|
24
26
|
import { breathingVisualizer } from './tool/breathingVisualizer/entry';
|
|
25
27
|
import { caffeineTracker } from './tool/caffeineTracker/entry';
|
|
28
|
+
import { dailyProteinCalculator } from './tool/dailyProteinCalculator/entry';
|
|
26
29
|
import { daltonismSimulator } from './tool/daltonismSimulator/entry';
|
|
27
30
|
import { digestionStopwatch } from './tool/digestionStopwatch/entry';
|
|
28
31
|
import { epworthSleepinessScale } from './tool/epworthSleepinessScale/entry';
|
|
29
32
|
import { hydrationCalculator } from './tool/hydrationCalculator/entry';
|
|
30
33
|
import { leanBodyMassCalculator } from './tool/leanBodyMassCalculator/entry';
|
|
34
|
+
import { metabolicAgeCalculator } from './tool/metabolicAgeCalculator/entry';
|
|
31
35
|
import { pelliRobsonTest } from './tool/pelliRobsonTest/entry';
|
|
32
36
|
import { peripheralVisionTrainer } from './tool/peripheralVisionTrainer/entry';
|
|
33
37
|
import { readingDistanceCalculator } from './tool/readingDistanceCalculator/entry';
|
|
@@ -35,4 +39,4 @@ import { screenDecompressionTime } from './tool/screenDecompressionTime/entry';
|
|
|
35
39
|
import { tinnitusReliever } from './tool/tinnitusReliever/entry';
|
|
36
40
|
import { ubeCalculator } from './tool/ubeCalculator/entry';
|
|
37
41
|
import { waterPurifier } from './tool/waterPurifier/entry';
|
|
38
|
-
export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, bodyAdiposityIndexCalculator, breathingVisualizer, caffeineTracker, daltonismSimulator, digestionStopwatch, epworthSleepinessScale, hydrationCalculator, leanBodyMassCalculator, pelliRobsonTest, peripheralVisionTrainer, readingDistanceCalculator, screenDecompressionTime, tinnitusReliever, ubeCalculator, waterPurifier];
|
|
42
|
+
export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, bodyAdiposityIndexCalculator, breathingVisualizer, caffeineTracker, dailyProteinCalculator, daltonismSimulator, digestionStopwatch, epworthSleepinessScale, hydrationCalculator, leanBodyMassCalculator, metabolicAgeCalculator, pelliRobsonTest, peripheralVisionTrainer, readingDistanceCalculator, screenDecompressionTime, tinnitusReliever, ubeCalculator, waterPurifier];
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,9 @@ export * from './tool/bloodUnitConverter';
|
|
|
15
15
|
export * from './tool/readingDistanceCalculator';
|
|
16
16
|
export * from './tool/hydrationCalculator';
|
|
17
17
|
export * from './tool/bodyAdiposityIndexCalculator';
|
|
18
|
+
export * from './tool/dailyProteinCalculator';
|
|
19
|
+
export * from './tool/leanBodyMassCalculator';
|
|
20
|
+
export * from './tool/metabolicAgeCalculator';
|
|
18
21
|
|
|
19
22
|
export { healthCategory } from './category';
|
|
20
23
|
export const healthCategorySEO = () => import('./category/seo.astro').then((m) => m.default);
|
|
@@ -5,7 +5,7 @@ import { healthCategory } from '../data';
|
|
|
5
5
|
describe('Tool Validation Suite', () => {
|
|
6
6
|
describe('Library Registration', () => {
|
|
7
7
|
it('should have 18 tools in ALL_TOOLS', () => {
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(20);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('healthCategory should be defined', () => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { dailyProteinCalculator } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const content = await dailyProteinCalculator.i18n[locale]?.();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && <Bibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'Phillips SM, Chevalier S, Leidy HJ: Protein requirements beyond the RDA',
|
|
6
|
+
url: 'https://doi.org/10.1139/apnm-2015-0550',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'Campbell B. et al.: International Society of Sports Nutrition position stand: protein and exercise',
|
|
10
|
+
url: 'https://doi.org/10.1186/1550-2783-4-8',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'Jager R. et al.: International Society of Sports Nutrition position stand: protein and exercise update',
|
|
14
|
+
url: 'https://doi.org/10.1186/s12970-017-0177-8',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'Institute of Medicine: Dietary Reference Intakes for Energy, Carbohydrate, Fiber, Fat, Fatty Acids, Cholesterol, Protein, and Amino Acids',
|
|
18
|
+
url: 'https://nap.nationalacademies.org/catalog/10490/dietary-reference-intakes-for-energy-carbohydrate-fiber-fat-fatty-acids-cholesterol-protein-and-amino-acids',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'NIH Office of Dietary Supplements: Protein fact sheet',
|
|
22
|
+
url: 'https://ods.od.nih.gov/factsheets/Protein-HealthProfessional/',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'Wikipedia: Protein in nutrition',
|
|
26
|
+
url: 'https://en.wikipedia.org/wiki/Protein_(nutrient)',
|
|
27
|
+
},
|
|
28
|
+
];
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Icon } from 'astro-icon/components';
|
|
3
|
+
import type { DailyProteinCalculatorUI } from './ui';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
locale?: string;
|
|
7
|
+
ui?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const t = (Astro.props.ui || {}) as DailyProteinCalculatorUI;
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<div class="dpc" data-ui={JSON.stringify(t)}>
|
|
14
|
+
<div class="dpc__shell">
|
|
15
|
+
<section class="dpc__result" aria-live="polite">
|
|
16
|
+
<div class="dpc__plate" aria-hidden="true">
|
|
17
|
+
<div class="dpc__plate-ring"></div>
|
|
18
|
+
<Icon name="mdi:food-steak" />
|
|
19
|
+
</div>
|
|
20
|
+
<div class="dpc__target">
|
|
21
|
+
<span>{t.dailyTargetLabel}</span>
|
|
22
|
+
<strong id="dpc-total">132 g</strong>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="dpc__stats">
|
|
25
|
+
<div><span>{t.rangeLabel}</span><strong id="dpc-range">112-151 g</strong></div>
|
|
26
|
+
<div><span>{t.ratioLabel}</span><strong id="dpc-ratio">1.8 g/kg</strong></div>
|
|
27
|
+
</div>
|
|
28
|
+
<div id="dpc-safe" class="dpc__safe" hidden>
|
|
29
|
+
<Icon name="mdi:alert-outline" />
|
|
30
|
+
<div><strong>{t.safeLimitTitle}</strong><p>{t.safeLimitText}</p></div>
|
|
31
|
+
</div>
|
|
32
|
+
</section>
|
|
33
|
+
|
|
34
|
+
<section class="dpc__controls">
|
|
35
|
+
<div class="dpc__topline">
|
|
36
|
+
<div class="dpc__seg" role="group" aria-label={t.unitSystemLabel}>
|
|
37
|
+
<button class="dpc__seg-btn dpc__seg-btn--active" type="button" data-unit="metric">{t.unitMetric}</button>
|
|
38
|
+
<button class="dpc__seg-btn" type="button" data-unit="imperial">{t.unitImperial}</button>
|
|
39
|
+
</div>
|
|
40
|
+
<label class="dpc__toggle">
|
|
41
|
+
<input id="dpc-pregnant" type="checkbox" />
|
|
42
|
+
<span>{t.pregnancyLabel}</span>
|
|
43
|
+
</label>
|
|
44
|
+
<label class="dpc__toggle">
|
|
45
|
+
<input id="dpc-lactating" type="checkbox" />
|
|
46
|
+
<span>{t.lactationLabel}</span>
|
|
47
|
+
</label>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="dpc__field">
|
|
51
|
+
<label for="dpc-weight"><b>{t.weightLabel}</b><output id="dpc-weight-output">75 kg</output></label>
|
|
52
|
+
<input id="dpc-weight" class="dpc__range" type="range" min="40" max="180" value="75" />
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div class="dpc__field">
|
|
56
|
+
<label for="dpc-age"><b>{t.ageLabel}</b><output id="dpc-age-output">38 years</output></label>
|
|
57
|
+
<input id="dpc-age" class="dpc__range" type="range" min="16" max="90" value="38" />
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div>
|
|
61
|
+
<span class="dpc__label">{t.goalLabel}</span>
|
|
62
|
+
<div class="dpc__chips" role="group" aria-label={t.goalLabel}>
|
|
63
|
+
<button class="dpc__chip" type="button" data-goal="fatLoss">{t.goalFatLoss}</button>
|
|
64
|
+
<button class="dpc__chip dpc__chip--active" type="button" data-goal="maintenance">{t.goalMaintenance}</button>
|
|
65
|
+
<button class="dpc__chip" type="button" data-goal="muscleGain">{t.goalMuscleGain}</button>
|
|
66
|
+
<button class="dpc__chip" type="button" data-goal="recovery">{t.goalRecovery}</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div>
|
|
71
|
+
<span class="dpc__label">{t.activityLabel}</span>
|
|
72
|
+
<div class="dpc__chips" role="group" aria-label={t.activityLabel}>
|
|
73
|
+
<button class="dpc__chip" type="button" data-activity="sedentary">{t.activitySedentary}</button>
|
|
74
|
+
<button class="dpc__chip dpc__chip--active" type="button" data-activity="active">{t.activityActive}</button>
|
|
75
|
+
<button class="dpc__chip" type="button" data-activity="endurance">{t.activityEndurance}</button>
|
|
76
|
+
<button class="dpc__chip" type="button" data-activity="strength">{t.activityStrength}</button>
|
|
77
|
+
<button class="dpc__chip" type="button" data-activity="athlete">{t.activityAthlete}</button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</section>
|
|
81
|
+
<section class="dpc__foods">
|
|
82
|
+
<h3>{t.foodPanelTitle}</h3>
|
|
83
|
+
<div class="dpc__food-grid">
|
|
84
|
+
<div><Icon name="mdi:food-drumstick" /><span>{t.chickenLabel}</span><strong id="dpc-chicken">120 g</strong></div>
|
|
85
|
+
<div><Icon name="mdi:soy-sauce" /><span>{t.tofuLabel}</span><strong id="dpc-tofu">310 g</strong></div>
|
|
86
|
+
<div><Icon name="mdi:egg" /><span>{t.eggsLabel}</span><strong id="dpc-eggs">5</strong></div>
|
|
87
|
+
<div><Icon name="mdi:bowl-mix" /><span>{t.greekYogurtLabel}</span><strong id="dpc-yogurt">270 g</strong></div>
|
|
88
|
+
<div><Icon name="mdi:spoon-sugar" /><span>{t.proteinPowderLabel}</span><strong id="dpc-powder">5 {t.scoops}</strong></div>
|
|
89
|
+
<div><Icon name="mdi:fish" /><span>{t.tunaLabel}</span><strong id="dpc-tuna">120 {t.grams}</strong></div>
|
|
90
|
+
</div>
|
|
91
|
+
<p><strong>{t.educationTitle}</strong> {t.educationText}</p>
|
|
92
|
+
</section>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<script>
|
|
97
|
+
type Unit = 'metric' | 'imperial';
|
|
98
|
+
type Goal = 'fatLoss' | 'maintenance' | 'muscleGain' | 'recovery';
|
|
99
|
+
type Activity = 'sedentary' | 'active' | 'endurance' | 'strength' | 'athlete';
|
|
100
|
+
|
|
101
|
+
interface CalculatorState {
|
|
102
|
+
unit: Unit;
|
|
103
|
+
weightKg: number;
|
|
104
|
+
age: number;
|
|
105
|
+
goal: Goal;
|
|
106
|
+
activity: Activity;
|
|
107
|
+
pregnant: boolean;
|
|
108
|
+
lactating: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const activityRanges: Record<Activity, [number, number]> = {
|
|
112
|
+
sedentary: [0.8, 1],
|
|
113
|
+
active: [1.2, 1.5],
|
|
114
|
+
endurance: [1.2, 1.6],
|
|
115
|
+
strength: [1.6, 2.2],
|
|
116
|
+
athlete: [1.8, 2.3],
|
|
117
|
+
};
|
|
118
|
+
const goalBoost: Record<Goal, [number, number]> = {
|
|
119
|
+
maintenance: [0, 0],
|
|
120
|
+
recovery: [0.2, 0.3],
|
|
121
|
+
muscleGain: [0.2, 0.4],
|
|
122
|
+
fatLoss: [0.5, 0.7],
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
function requireEl<T extends Element>(root: Element, selector: string): T {
|
|
126
|
+
const el = root.querySelector<T>(selector);
|
|
127
|
+
if (!el) throw new Error(`Missing ${selector}`);
|
|
128
|
+
return el;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function kgToLb(kg: number) { return kg * 2.2046226218; }
|
|
132
|
+
function lbToKg(lb: number) { return lb / 2.2046226218; }
|
|
133
|
+
|
|
134
|
+
function calculate(state: CalculatorState) {
|
|
135
|
+
const [activityLow, activityHigh] = activityRanges[state.activity];
|
|
136
|
+
const [boostLow, boostHigh] = goalBoost[state.goal];
|
|
137
|
+
const ageBoost = state.age >= 65 ? 0.2 : 0;
|
|
138
|
+
const low = Math.min(2.4, activityLow + boostLow + ageBoost);
|
|
139
|
+
const high = Math.min(2.6, activityHigh + boostHigh + ageBoost);
|
|
140
|
+
const ratio = Math.round(((low + high) / 2) * 10) / 10;
|
|
141
|
+
const added = (state.pregnant ? 25 : 0) + (state.lactating ? 20 : 0);
|
|
142
|
+
const total = Math.round(state.weightKg * ratio + added);
|
|
143
|
+
return { total, low: Math.round(state.weightKg * low + added), high: Math.round(state.weightKg * high + added), ratio };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function syncWeightControl(state: CalculatorState, weight: HTMLInputElement) {
|
|
147
|
+
if (state.unit === 'imperial') {
|
|
148
|
+
weight.min = '88';
|
|
149
|
+
weight.max = '397';
|
|
150
|
+
weight.value = String(Math.round(kgToLb(state.weightKg)));
|
|
151
|
+
} else {
|
|
152
|
+
weight.min = '40';
|
|
153
|
+
weight.max = '180';
|
|
154
|
+
weight.value = String(Math.round(state.weightKg));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function renderFoods(root: Element, total: number, ui: Record<string, string>) {
|
|
159
|
+
requireEl<HTMLElement>(root, '#dpc-chicken').textContent = `${Math.round((total / 31) * 100)} ${ui.grams}`;
|
|
160
|
+
requireEl<HTMLElement>(root, '#dpc-tofu').textContent = `${Math.round((total / 12) * 100)} ${ui.grams}`;
|
|
161
|
+
requireEl<HTMLElement>(root, '#dpc-eggs').textContent = `${Math.max(1, Math.round(total / 6))}`;
|
|
162
|
+
requireEl<HTMLElement>(root, '#dpc-yogurt').textContent = `${Math.round((total / 10) * 100)} ${ui.grams}`;
|
|
163
|
+
requireEl<HTMLElement>(root, '#dpc-powder').textContent = `${(total / 25).toFixed(1)} ${ui.scoops}`;
|
|
164
|
+
requireEl<HTMLElement>(root, '#dpc-tuna').textContent = `${Math.round((total / 26) * 100)} ${ui.grams}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function loadSavedState(state: CalculatorState) {
|
|
168
|
+
const saved = localStorage.getItem('dpc_state');
|
|
169
|
+
if (!saved) return;
|
|
170
|
+
try {
|
|
171
|
+
const parsed = JSON.parse(saved);
|
|
172
|
+
if (parsed.unit) state.unit = parsed.unit;
|
|
173
|
+
if (parsed.weightKg) state.weightKg = parsed.weightKg;
|
|
174
|
+
if (parsed.age) state.age = parsed.age;
|
|
175
|
+
if (parsed.goal) state.goal = parsed.goal;
|
|
176
|
+
if (parsed.activity) state.activity = parsed.activity;
|
|
177
|
+
state.pregnant = !!parsed.pregnant;
|
|
178
|
+
state.lactating = !!parsed.lactating;
|
|
179
|
+
} catch {}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function setupInputs(root: Element, state: CalculatorState) {
|
|
183
|
+
const weight = requireEl<HTMLInputElement>(root, '#dpc-weight');
|
|
184
|
+
const age = requireEl<HTMLInputElement>(root, '#dpc-age');
|
|
185
|
+
const pregnant = requireEl<HTMLInputElement>(root, '#dpc-pregnant');
|
|
186
|
+
const lactating = requireEl<HTMLInputElement>(root, '#dpc-lactating');
|
|
187
|
+
|
|
188
|
+
weight.value = String(state.unit === 'imperial' ? Math.round(kgToLb(state.weightKg)) : Math.round(state.weightKg));
|
|
189
|
+
age.value = String(state.age);
|
|
190
|
+
pregnant.checked = state.pregnant;
|
|
191
|
+
lactating.checked = state.lactating;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function updateActiveClasses(root: Element, state: CalculatorState) {
|
|
195
|
+
root.querySelectorAll<HTMLButtonElement>('[data-unit]').forEach((item) => {
|
|
196
|
+
item.classList.toggle('dpc__seg-btn--active', item.dataset.unit === state.unit);
|
|
197
|
+
});
|
|
198
|
+
root.querySelectorAll<HTMLButtonElement>('[data-goal]').forEach((item) => {
|
|
199
|
+
item.classList.toggle('dpc__chip--active', item.dataset.goal === state.goal);
|
|
200
|
+
});
|
|
201
|
+
root.querySelectorAll<HTMLButtonElement>('[data-activity]').forEach((item) => {
|
|
202
|
+
item.classList.toggle('dpc__chip--active', item.dataset.activity === state.activity);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function renderAll(root: Element, state: CalculatorState, ui: Record<string, string>) {
|
|
207
|
+
const result = calculate(state);
|
|
208
|
+
requireEl<HTMLElement>(root, '#dpc-total').textContent = `${result.total} ${ui.grams}`;
|
|
209
|
+
requireEl<HTMLElement>(root, '#dpc-range').textContent = `${result.low}-${result.high} ${ui.grams}`;
|
|
210
|
+
requireEl<HTMLElement>(root, '#dpc-ratio').textContent = `${result.ratio.toFixed(1)} g/kg`;
|
|
211
|
+
requireEl<HTMLOutputElement>(root, '#dpc-weight-output').textContent = state.unit === 'imperial' ? `${Math.round(kgToLb(state.weightKg))} ${ui.lb}` : `${Math.round(state.weightKg)} ${ui.kg}`;
|
|
212
|
+
requireEl<HTMLOutputElement>(root, '#dpc-age-output').textContent = `${state.age} ${ui.years}`;
|
|
213
|
+
requireEl<HTMLElement>(root, '#dpc-safe').hidden = result.total / state.weightKg <= 2.5;
|
|
214
|
+
renderFoods(root, result.total, ui);
|
|
215
|
+
localStorage.setItem('dpc_state', JSON.stringify(state));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function bindButtons(root: Element, state: CalculatorState, ui: Record<string, string>, weight: HTMLInputElement) {
|
|
219
|
+
root.querySelectorAll<HTMLButtonElement>('[data-unit]').forEach((btn) => {
|
|
220
|
+
btn.addEventListener('click', () => {
|
|
221
|
+
state.unit = btn.dataset.unit as Unit;
|
|
222
|
+
updateActiveClasses(root, state);
|
|
223
|
+
syncWeightControl(state, weight);
|
|
224
|
+
renderAll(root, state, ui);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
root.querySelectorAll<HTMLButtonElement>('[data-goal]').forEach((btn) => {
|
|
228
|
+
btn.addEventListener('click', () => {
|
|
229
|
+
state.goal = btn.dataset.goal as Goal;
|
|
230
|
+
updateActiveClasses(root, state);
|
|
231
|
+
renderAll(root, state, ui);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
root.querySelectorAll<HTMLButtonElement>('[data-activity]').forEach((btn) => {
|
|
235
|
+
btn.addEventListener('click', () => {
|
|
236
|
+
state.activity = btn.dataset.activity as Activity;
|
|
237
|
+
updateActiveClasses(root, state);
|
|
238
|
+
renderAll(root, state, ui);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function bindInputs(root: Element, state: CalculatorState, ui: Record<string, string>, weight: HTMLInputElement) {
|
|
244
|
+
const age = requireEl<HTMLInputElement>(root, '#dpc-age');
|
|
245
|
+
const pregnant = requireEl<HTMLInputElement>(root, '#dpc-pregnant');
|
|
246
|
+
const lactating = requireEl<HTMLInputElement>(root, '#dpc-lactating');
|
|
247
|
+
|
|
248
|
+
weight.addEventListener('input', () => {
|
|
249
|
+
state.weightKg = state.unit === 'imperial' ? lbToKg(Number(weight.value)) : Number(weight.value);
|
|
250
|
+
renderAll(root, state, ui);
|
|
251
|
+
});
|
|
252
|
+
age.addEventListener('input', () => {
|
|
253
|
+
state.age = Number(age.value);
|
|
254
|
+
renderAll(root, state, ui);
|
|
255
|
+
});
|
|
256
|
+
pregnant.addEventListener('change', () => {
|
|
257
|
+
state.pregnant = pregnant.checked;
|
|
258
|
+
renderAll(root, state, ui);
|
|
259
|
+
});
|
|
260
|
+
lactating.addEventListener('change', () => {
|
|
261
|
+
state.lactating = lactating.checked;
|
|
262
|
+
renderAll(root, state, ui);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function initDailyProtein(root: Element) {
|
|
267
|
+
const ui = JSON.parse((root as HTMLElement).dataset.ui || '{}') as Record<string, string>;
|
|
268
|
+
const state: CalculatorState = { unit: 'metric', weightKg: 75, age: 38, goal: 'maintenance', activity: 'active', pregnant: false, lactating: false };
|
|
269
|
+
const weight = requireEl<HTMLInputElement>(root, '#dpc-weight');
|
|
270
|
+
|
|
271
|
+
loadSavedState(state);
|
|
272
|
+
setupInputs(root, state);
|
|
273
|
+
updateActiveClasses(root, state);
|
|
274
|
+
syncWeightControl(state, weight);
|
|
275
|
+
bindButtons(root, state, ui, weight);
|
|
276
|
+
bindInputs(root, state, ui, weight);
|
|
277
|
+
renderAll(root, state, ui);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
document.querySelectorAll('.dpc').forEach(initDailyProtein);
|
|
281
|
+
</script>
|
|
282
|
+
|