@jjlmoya/utils-health 1.25.0 → 1.26.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 +2 -1
- package/src/entries.ts +3 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/targetHeightEstimator/bibliography.astro +14 -0
- package/src/tool/targetHeightEstimator/bibliography.ts +28 -0
- package/src/tool/targetHeightEstimator/component.astro +335 -0
- package/src/tool/targetHeightEstimator/entry.ts +27 -0
- package/src/tool/targetHeightEstimator/i18n/de.ts +330 -0
- package/src/tool/targetHeightEstimator/i18n/en.ts +330 -0
- package/src/tool/targetHeightEstimator/i18n/es.ts +330 -0
- package/src/tool/targetHeightEstimator/i18n/fr.ts +330 -0
- package/src/tool/targetHeightEstimator/i18n/id.ts +330 -0
- package/src/tool/targetHeightEstimator/i18n/it.ts +330 -0
- package/src/tool/targetHeightEstimator/i18n/ja.ts +330 -0
- package/src/tool/targetHeightEstimator/i18n/ko.ts +330 -0
- package/src/tool/targetHeightEstimator/i18n/nl.ts +330 -0
- package/src/tool/targetHeightEstimator/i18n/pl.ts +330 -0
- package/src/tool/targetHeightEstimator/i18n/pt.ts +330 -0
- package/src/tool/targetHeightEstimator/i18n/ru.ts +330 -0
- package/src/tool/targetHeightEstimator/i18n/sv.ts +330 -0
- package/src/tool/targetHeightEstimator/i18n/tr.ts +330 -0
- package/src/tool/targetHeightEstimator/i18n/zh.ts +330 -0
- package/src/tool/targetHeightEstimator/index.ts +12 -0
- package/src/tool/targetHeightEstimator/logic.ts +27 -0
- package/src/tool/targetHeightEstimator/seo.astro +15 -0
- package/src/tool/targetHeightEstimator/target-height-estimator.css +407 -0
- package/src/tool/targetHeightEstimator/ui.ts +31 -0
- package/src/tools.ts +2 -1
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -21,10 +21,11 @@ import { epworthSleepinessScale } from '../tool/epworthSleepinessScale/entry';
|
|
|
21
21
|
import { leanBodyMassCalculator } from '../tool/leanBodyMassCalculator/entry';
|
|
22
22
|
import { metabolicAgeCalculator } from '../tool/metabolicAgeCalculator/entry';
|
|
23
23
|
import { bunCreatinineCalculator } from '../tool/bunCreatinineCalculator/entry';
|
|
24
|
+
import { targetHeightEstimator } from '../tool/targetHeightEstimator/entry';
|
|
24
25
|
|
|
25
26
|
export const healthCategory: HealthCategoryEntry = {
|
|
26
27
|
icon: 'mdi:heart-pulse',
|
|
27
|
-
tools: [bmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, leanBodyMassCalculator, metabolicAgeCalculator, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, daltonismSimulator, tinnitusReliever, breathingVisualizer, caffeineTracker, waterPurifier, digestionStopwatch, binauralTuner, pelliRobsonTest, peripheralVisionTrainer, epworthSleepinessScale],
|
|
28
|
+
tools: [bmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, leanBodyMassCalculator, metabolicAgeCalculator, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, targetHeightEstimator, daltonismSimulator, tinnitusReliever, breathingVisualizer, caffeineTracker, waterPurifier, digestionStopwatch, binauralTuner, pelliRobsonTest, peripheralVisionTrainer, epworthSleepinessScale],
|
|
28
29
|
i18n: {
|
|
29
30
|
en: () => import('./i18n/en').then((m) => m.content),
|
|
30
31
|
es: () => import('./i18n/es').then((m) => m.content),
|
package/src/entries.ts
CHANGED
|
@@ -18,6 +18,7 @@ export { metabolicAgeCalculator } from './tool/metabolicAgeCalculator/entry';
|
|
|
18
18
|
export { macronutrientCalculator } from './tool/macronutrientCalculator/entry';
|
|
19
19
|
export { tdeeCalculator } from './tool/tdeeCalculator/entry';
|
|
20
20
|
export { bunCreatinineCalculator } from './tool/bunCreatinineCalculator/entry';
|
|
21
|
+
export { targetHeightEstimator } from './tool/targetHeightEstimator/entry';
|
|
21
22
|
export { pelliRobsonTest } from './tool/pelliRobsonTest/entry';
|
|
22
23
|
export { peripheralVisionTrainer } from './tool/peripheralVisionTrainer/entry';
|
|
23
24
|
export { readingDistanceCalculator } from './tool/readingDistanceCalculator/entry';
|
|
@@ -46,6 +47,7 @@ import { metabolicAgeCalculator } from './tool/metabolicAgeCalculator/entry';
|
|
|
46
47
|
import { macronutrientCalculator } from './tool/macronutrientCalculator/entry';
|
|
47
48
|
import { tdeeCalculator } from './tool/tdeeCalculator/entry';
|
|
48
49
|
import { bunCreatinineCalculator } from './tool/bunCreatinineCalculator/entry';
|
|
50
|
+
import { targetHeightEstimator } from './tool/targetHeightEstimator/entry';
|
|
49
51
|
import { pelliRobsonTest } from './tool/pelliRobsonTest/entry';
|
|
50
52
|
import { peripheralVisionTrainer } from './tool/peripheralVisionTrainer/entry';
|
|
51
53
|
import { readingDistanceCalculator } from './tool/readingDistanceCalculator/entry';
|
|
@@ -53,4 +55,4 @@ import { screenDecompressionTime } from './tool/screenDecompressionTime/entry';
|
|
|
53
55
|
import { tinnitusReliever } from './tool/tinnitusReliever/entry';
|
|
54
56
|
import { ubeCalculator } from './tool/ubeCalculator/entry';
|
|
55
57
|
import { waterPurifier } from './tool/waterPurifier/entry';
|
|
56
|
-
export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, breathingVisualizer, caffeineTracker, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, daltonismSimulator, digestionStopwatch, epworthSleepinessScale, hydrationCalculator, leanBodyMassCalculator, metabolicAgeCalculator, pelliRobsonTest, peripheralVisionTrainer, readingDistanceCalculator, screenDecompressionTime, tinnitusReliever, ubeCalculator, waterPurifier];
|
|
58
|
+
export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, breathingVisualizer, caffeineTracker, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, targetHeightEstimator, daltonismSimulator, digestionStopwatch, epworthSleepinessScale, hydrationCalculator, leanBodyMassCalculator, metabolicAgeCalculator, pelliRobsonTest, peripheralVisionTrainer, readingDistanceCalculator, screenDecompressionTime, tinnitusReliever, ubeCalculator, waterPurifier];
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ export * from './tool/bodyFatPercentageCalculator';
|
|
|
25
25
|
export * from './tool/macronutrientCalculator';
|
|
26
26
|
export * from './tool/tdeeCalculator';
|
|
27
27
|
export * from './tool/bunCreatinineCalculator';
|
|
28
|
+
export * from './tool/targetHeightEstimator';
|
|
28
29
|
|
|
29
30
|
export { healthCategory } from './category';
|
|
30
31
|
export const healthCategorySEO = () => import('./category/seo.astro').then((m) => m.default);
|
|
@@ -4,8 +4,8 @@ import { healthCategory } from '../data';
|
|
|
4
4
|
|
|
5
5
|
describe('Tool Validation Suite', () => {
|
|
6
6
|
describe('Library Registration', () => {
|
|
7
|
-
it('should have
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
7
|
+
it('should have 28 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(28);
|
|
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 { targetHeightEstimator } 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 targetHeightEstimator.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: "Tanner, Goldstein & Whitehouse: Standards for children's height allowing for height of parents",
|
|
6
|
+
url: 'https://doi.org/10.1136/adc.45.244.755',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'Luo, Albertsson-Wikland & Karlberg: Target height as predicted by parental heights for Swedish children',
|
|
10
|
+
url: 'https://doi.org/10.1203/00006450-199804000-00016',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'Merck Manual Professional: Physical growth of infants and children',
|
|
14
|
+
url: 'https://www.merckmanuals.com/professional/pediatrics/growth-and-development/physical-growth-of-infants-and-children',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'CDC: Clinical growth charts',
|
|
18
|
+
url: 'https://www.cdc.gov/growthcharts/clinical_charts.htm',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'WHO: Child growth standards',
|
|
22
|
+
url: 'https://www.who.int/tools/child-growth-standards',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'Wikipedia: Human height',
|
|
26
|
+
url: 'https://en.wikipedia.org/wiki/Human_height',
|
|
27
|
+
},
|
|
28
|
+
];
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Icon } from 'astro-icon/components';
|
|
3
|
+
import type { TargetHeightEstimatorUI } from './ui';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
locale?: string;
|
|
7
|
+
ui?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const t = (Astro.props.ui || {}) as TargetHeightEstimatorUI;
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<div class="the" data-ui={JSON.stringify(t)}>
|
|
14
|
+
<div class="the__shell">
|
|
15
|
+
<div class="the__stage">
|
|
16
|
+
<svg class="the__scene" viewBox="0 0 760 430" role="img" aria-label={t.scaleAriaLabel}>
|
|
17
|
+
<defs>
|
|
18
|
+
<linearGradient id="theRangeFill" x1="0" x2="1">
|
|
19
|
+
<stop offset="0" stop-color="#5fa8d3" stop-opacity="0.18" />
|
|
20
|
+
<stop offset="0.5" stop-color="#57c4a6" stop-opacity="0.32" />
|
|
21
|
+
<stop offset="1" stop-color="#f0b35b" stop-opacity="0.18" />
|
|
22
|
+
</linearGradient>
|
|
23
|
+
</defs>
|
|
24
|
+
<rect class="the__wall" x="20" y="18" width="720" height="360" rx="8" />
|
|
25
|
+
<g class="the__ticks" id="the-ticks"></g>
|
|
26
|
+
<path id="the-range-band" class="the__range-band" d="M110 170 C260 145 430 145 650 170 L650 220 C430 246 260 246 110 220 Z" />
|
|
27
|
+
<line id="the-target-line" class="the__target-line" x1="86" y1="194" x2="682" y2="194" />
|
|
28
|
+
<line class="the__floor" x1="82" y1="366" x2="700" y2="366" />
|
|
29
|
+
<g class="the__person the__person--mother" id="the-mother-figure" transform="translate(168 366) scale(0.92)">
|
|
30
|
+
<circle cx="0" cy="-270" r="27" />
|
|
31
|
+
<path class="the__hair" d="M-26 -266 C-33 -292 -14 -308 6 -304 C28 -300 37 -276 28 -254 C18 -270 -8 -282 -26 -266 Z" />
|
|
32
|
+
<path class="the__body" d="M-28 -232 C-12 -244 12 -244 28 -232 C42 -204 43 -158 34 -122 L45 -54 L18 -54 L13 0 L-13 0 L-18 -54 L-45 -54 L-34 -122 C-43 -158 -42 -204 -28 -232 Z" />
|
|
33
|
+
<path class="the__arm" d="M-32 -214 C-61 -180 -62 -128 -45 -84" />
|
|
34
|
+
<path class="the__arm" d="M32 -214 C61 -180 62 -128 45 -84" />
|
|
35
|
+
<path class="the__leg" d="M-16 -54 L-20 0" />
|
|
36
|
+
<path class="the__leg" d="M16 -54 L20 0" />
|
|
37
|
+
</g>
|
|
38
|
+
<g class="the__person the__person--child" id="the-child" data-sex="boy" transform="translate(386 366) scale(0.86)">
|
|
39
|
+
<circle cx="0" cy="-258" r="25" />
|
|
40
|
+
<path class="the__hair the__hair--boy" d="M-24 -260 C-21 -282 -1 -293 20 -284 C29 -279 32 -264 27 -250 C14 -260 -7 -266 -24 -260 Z" />
|
|
41
|
+
<path class="the__hair the__hair--girl" d="M-25 -260 C-22 -284 -2 -297 20 -286 C31 -280 35 -262 27 -248 C15 -263 -7 -270 -25 -260 Z" />
|
|
42
|
+
<circle class="the__ponytail" cx="31" cy="-250" r="10" />
|
|
43
|
+
<path class="the__body the__body--boy" d="M-25 -218 C-10 -229 10 -229 25 -218 C34 -194 34 -154 24 -118 L-24 -118 C-34 -154 -34 -194 -25 -218 Z" />
|
|
44
|
+
<path class="the__pants the__pants--boy" d="M-22 -118 L-5 -118 L-9 -2 L-24 -2 Z" />
|
|
45
|
+
<path class="the__pants the__pants--boy" d="M5 -118 L22 -118 L24 -2 L9 -2 Z" />
|
|
46
|
+
<path class="the__body the__body--girl" d="M-25 -218 C-10 -229 10 -229 25 -218 C37 -190 38 -148 30 -116 L36 -58 L14 -58 L10 0 L-10 0 L-14 -58 L-36 -58 L-30 -116 C-38 -148 -37 -190 -25 -218 Z" />
|
|
47
|
+
<path class="the__arm" d="M-29 -202 C-52 -170 -54 -126 -40 -90" />
|
|
48
|
+
<path class="the__arm" d="M29 -202 C52 -170 54 -126 40 -90" />
|
|
49
|
+
<path class="the__leg the__leg--boy" d="M-16 -2 L-18 0" />
|
|
50
|
+
<path class="the__leg the__leg--boy" d="M16 -2 L18 0" />
|
|
51
|
+
<path class="the__leg the__leg--girl" d="M-12 -58 L-15 0" />
|
|
52
|
+
<path class="the__leg the__leg--girl" d="M12 -58 L15 0" />
|
|
53
|
+
</g>
|
|
54
|
+
<g class="the__person the__person--father" id="the-father-figure" transform="translate(600 366) scale(1.02)">
|
|
55
|
+
<circle cx="0" cy="-276" r="29" />
|
|
56
|
+
<path class="the__hair" d="M-27 -278 C-18 -305 14 -306 29 -282 C17 -292 -8 -294 -27 -278 Z" />
|
|
57
|
+
<path class="the__body" d="M-31 -236 C-13 -248 13 -248 31 -236 C43 -206 42 -158 34 -120 L28 0 L10 0 L5 -72 L-5 -72 L-10 0 L-28 0 L-34 -120 C-42 -158 -43 -206 -31 -236 Z" />
|
|
58
|
+
<path class="the__arm" d="M-35 -218 C-66 -182 -68 -126 -49 -82" />
|
|
59
|
+
<path class="the__arm" d="M35 -218 C66 -182 68 -126 49 -82" />
|
|
60
|
+
<path class="the__leg" d="M-10 -72 L-13 0" />
|
|
61
|
+
<path class="the__leg" d="M10 -72 L13 0" />
|
|
62
|
+
</g>
|
|
63
|
+
<text class="the__figure-label" x="166" y="402">{t.motherFigure}</text>
|
|
64
|
+
<text class="the__figure-label" x="386" y="402">{t.childFigure}</text>
|
|
65
|
+
<text class="the__figure-label" x="594" y="402">{t.fatherFigure}</text>
|
|
66
|
+
<text class="the__scale-label" x="44" y="38">{t.wallScale}</text>
|
|
67
|
+
</svg>
|
|
68
|
+
<div class="the__result">
|
|
69
|
+
<span>{t.targetLabel}</span>
|
|
70
|
+
<strong id="the-target">{t.targetInitial}</strong>
|
|
71
|
+
<em id="the-range">{t.rangeInitial}</em>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="the__panel">
|
|
76
|
+
<div class="the__toggles">
|
|
77
|
+
<div class="the__unit" role="group" aria-label={t.unitSystemLabel}>
|
|
78
|
+
<button class="the__unit-btn the__unit-btn--active" type="button" data-unit="metric">{t.unitMetric}</button>
|
|
79
|
+
<button class="the__unit-btn" type="button" data-unit="imperial">{t.unitImperial}</button>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="the__sex" role="group" aria-label={t.labelSex}>
|
|
82
|
+
<button class="the__sex-btn the__sex-btn--active" type="button" data-sex="boy" title={t.labelBoy}>
|
|
83
|
+
<Icon name="mdi:gender-male" />
|
|
84
|
+
</button>
|
|
85
|
+
<button class="the__sex-btn" type="button" data-sex="girl" title={t.labelGirl}>
|
|
86
|
+
<Icon name="mdi:gender-female" />
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<label class="the__field">
|
|
92
|
+
<span><span>{t.labelMother}</span><strong id="the-mother-value">{t.motherInitial}</strong></span>
|
|
93
|
+
<input id="the-mother" class="the__range" type="range" min="140" max="190" value="162" />
|
|
94
|
+
</label>
|
|
95
|
+
|
|
96
|
+
<label class="the__field">
|
|
97
|
+
<span><span>{t.labelFather}</span><strong id="the-father-value">{t.fatherInitial}</strong></span>
|
|
98
|
+
<input id="the-father" class="the__range" type="range" min="150" max="205" value="176" />
|
|
99
|
+
</label>
|
|
100
|
+
|
|
101
|
+
<div class="the__cards">
|
|
102
|
+
<div class="the__card">
|
|
103
|
+
<Icon name="mdi:function-variant" />
|
|
104
|
+
<p><strong>{t.formulaTitle}</strong> <span id="the-formula">{t.formulaBoy}</span></p>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="the__card">
|
|
107
|
+
<Icon name="mdi:chart-bell-curve" />
|
|
108
|
+
<p><strong>{t.rangeTitle}</strong> {t.rangeText}</p>
|
|
109
|
+
</div>
|
|
110
|
+
<div class="the__card the__card--note">
|
|
111
|
+
<Icon name="mdi:stethoscope" />
|
|
112
|
+
<p><strong>{t.clinicalNoteTitle}</strong> {t.clinicalNoteText}</p>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<script>
|
|
120
|
+
type Sex = 'boy' | 'girl';
|
|
121
|
+
type Unit = 'metric' | 'imperial';
|
|
122
|
+
|
|
123
|
+
interface Elements {
|
|
124
|
+
mother: HTMLInputElement;
|
|
125
|
+
father: HTMLInputElement;
|
|
126
|
+
motherValue: HTMLElement;
|
|
127
|
+
fatherValue: HTMLElement;
|
|
128
|
+
target: HTMLElement;
|
|
129
|
+
range: HTMLElement;
|
|
130
|
+
formula: HTMLElement;
|
|
131
|
+
child: SVGGElement;
|
|
132
|
+
motherFigure: SVGGElement;
|
|
133
|
+
fatherFigure: SVGGElement;
|
|
134
|
+
targetLine: SVGLineElement;
|
|
135
|
+
rangeBand: SVGPathElement;
|
|
136
|
+
ticks: SVGGElement;
|
|
137
|
+
unitBtns: NodeListOf<HTMLButtonElement>;
|
|
138
|
+
sexBtns: NodeListOf<HTMLButtonElement>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface State {
|
|
142
|
+
unit: Unit;
|
|
143
|
+
sex: Sex;
|
|
144
|
+
motherCm: number;
|
|
145
|
+
fatherCm: number;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const STORAGE_KEY = 'target_height_estimator_state';
|
|
149
|
+
const RANGE_CM = 8.5;
|
|
150
|
+
|
|
151
|
+
function required<T extends Element>(container: Element, selector: string): T {
|
|
152
|
+
const element = container.querySelector<T>(selector);
|
|
153
|
+
if (!element) throw new Error(selector);
|
|
154
|
+
return element;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getElements(container: Element): Elements | null {
|
|
158
|
+
try {
|
|
159
|
+
return {
|
|
160
|
+
mother: required<HTMLInputElement>(container, '#the-mother'),
|
|
161
|
+
father: required<HTMLInputElement>(container, '#the-father'),
|
|
162
|
+
motherValue: required<HTMLElement>(container, '#the-mother-value'),
|
|
163
|
+
fatherValue: required<HTMLElement>(container, '#the-father-value'),
|
|
164
|
+
target: required<HTMLElement>(container, '#the-target'),
|
|
165
|
+
range: required<HTMLElement>(container, '#the-range'),
|
|
166
|
+
formula: required<HTMLElement>(container, '#the-formula'),
|
|
167
|
+
child: required<SVGGElement>(container, '#the-child'),
|
|
168
|
+
motherFigure: required<SVGGElement>(container, '#the-mother-figure'),
|
|
169
|
+
fatherFigure: required<SVGGElement>(container, '#the-father-figure'),
|
|
170
|
+
targetLine: required<SVGLineElement>(container, '#the-target-line'),
|
|
171
|
+
rangeBand: required<SVGPathElement>(container, '#the-range-band'),
|
|
172
|
+
ticks: required<SVGGElement>(container, '#the-ticks'),
|
|
173
|
+
unitBtns: container.querySelectorAll<HTMLButtonElement>('.the__unit-btn'),
|
|
174
|
+
sexBtns: container.querySelectorAll<HTMLButtonElement>('.the__sex-btn'),
|
|
175
|
+
};
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function cmToIn(cm: number): number {
|
|
182
|
+
return cm / 2.54;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function inToCm(inches: number): number {
|
|
186
|
+
return inches * 2.54;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function clamp(value: number, min: number, max: number): number {
|
|
190
|
+
return Math.max(min, Math.min(max, value));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isUnit(value: unknown): value is Unit {
|
|
194
|
+
return value === 'metric' || value === 'imperial';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function isSex(value: unknown): value is Sex {
|
|
198
|
+
return value === 'boy' || value === 'girl';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function loadState(): State {
|
|
202
|
+
const fallback: State = { unit: 'metric', sex: 'boy', motherCm: 162, fatherCm: 176 };
|
|
203
|
+
try {
|
|
204
|
+
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
205
|
+
if (!raw) return fallback;
|
|
206
|
+
const saved = JSON.parse(raw) as Partial<State>;
|
|
207
|
+
return {
|
|
208
|
+
unit: isUnit(saved.unit) ? saved.unit : fallback.unit,
|
|
209
|
+
sex: isSex(saved.sex) ? saved.sex : fallback.sex,
|
|
210
|
+
motherCm: clamp(Number(saved.motherCm) || fallback.motherCm, 130, 210),
|
|
211
|
+
fatherCm: clamp(Number(saved.fatherCm) || fallback.fatherCm, 130, 220),
|
|
212
|
+
};
|
|
213
|
+
} catch {
|
|
214
|
+
return fallback;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function saveState(state: State) {
|
|
219
|
+
try {
|
|
220
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
221
|
+
} catch {
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function calculateTarget(motherCm: number, fatherCm: number, sex: Sex): number {
|
|
226
|
+
return (fatherCm + motherCm + (sex === 'boy' ? 13 : -13)) / 2;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function formatLength(cm: number, unit: Unit, ui: Record<string, string>): string {
|
|
230
|
+
if (unit === 'imperial') {
|
|
231
|
+
const totalInches = Math.round(cmToIn(cm));
|
|
232
|
+
return `${Math.floor(totalInches / 12)} ${ui.unitFt} ${totalInches % 12} ${ui.unitIn}`;
|
|
233
|
+
}
|
|
234
|
+
return `${Math.round(cm)} ${ui.unitCm}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function heightToY(cm: number): number {
|
|
238
|
+
return 354 - ((cm - 130) / 90) * 290;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function syncRanges(elements: Elements, state: State) {
|
|
242
|
+
if (state.unit === 'imperial') {
|
|
243
|
+
elements.mother.min = '55';
|
|
244
|
+
elements.mother.max = '75';
|
|
245
|
+
elements.mother.step = '0.5';
|
|
246
|
+
elements.mother.value = cmToIn(state.motherCm).toFixed(1);
|
|
247
|
+
elements.father.min = '59';
|
|
248
|
+
elements.father.max = '81';
|
|
249
|
+
elements.father.step = '0.5';
|
|
250
|
+
elements.father.value = cmToIn(state.fatherCm).toFixed(1);
|
|
251
|
+
} else {
|
|
252
|
+
elements.mother.min = '140';
|
|
253
|
+
elements.mother.max = '190';
|
|
254
|
+
elements.mother.step = '1';
|
|
255
|
+
elements.mother.value = String(Math.round(state.motherCm));
|
|
256
|
+
elements.father.min = '150';
|
|
257
|
+
elements.father.max = '205';
|
|
258
|
+
elements.father.step = '1';
|
|
259
|
+
elements.father.value = String(Math.round(state.fatherCm));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function syncButtons(elements: Elements, state: State) {
|
|
264
|
+
elements.unitBtns.forEach((btn) => btn.classList.toggle('the__unit-btn--active', btn.dataset.unit === state.unit));
|
|
265
|
+
elements.sexBtns.forEach((btn) => btn.classList.toggle('the__sex-btn--active', btn.dataset.sex === state.sex));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function renderTicks(elements: Elements, state: State, ui: Record<string, string>) {
|
|
269
|
+
const values = state.unit === 'imperial' ? [55, 60, 65, 70, 75, 80] : [140, 150, 160, 170, 180, 190, 200];
|
|
270
|
+
elements.ticks.innerHTML = values.map((value) => {
|
|
271
|
+
const cm = state.unit === 'imperial' ? inToCm(value) : value;
|
|
272
|
+
const y = heightToY(cm);
|
|
273
|
+
const label = state.unit === 'imperial' ? `${Math.floor(value / 12)} ${ui.unitFt} ${value % 12} ${ui.unitIn}` : `${value} ${ui.unitCm}`;
|
|
274
|
+
return `<g><line x1="46" y1="${y}" x2="710" y2="${y}" /><text x="54" y="${y - 6}">${label}</text></g>`;
|
|
275
|
+
}).join('');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function render(elements: Elements, state: State, ui: Record<string, string>) {
|
|
279
|
+
const target = calculateTarget(state.motherCm, state.fatherCm, state.sex);
|
|
280
|
+
const lower = target - RANGE_CM;
|
|
281
|
+
const upper = target + RANGE_CM;
|
|
282
|
+
const targetY = heightToY(target);
|
|
283
|
+
const upperY = heightToY(upper);
|
|
284
|
+
const lowerY = heightToY(lower);
|
|
285
|
+
const childScale = clamp(target / 175, 0.82, 1.2);
|
|
286
|
+
const motherScale = clamp(state.motherCm / 175, 0.78, 1.12);
|
|
287
|
+
const fatherScale = clamp(state.fatherCm / 175, 0.82, 1.2);
|
|
288
|
+
|
|
289
|
+
elements.motherValue.textContent = formatLength(state.motherCm, state.unit, ui);
|
|
290
|
+
elements.fatherValue.textContent = formatLength(state.fatherCm, state.unit, ui);
|
|
291
|
+
elements.target.textContent = formatLength(target, state.unit, ui);
|
|
292
|
+
elements.range.textContent = `${ui.rangeLabel}: ${formatLength(lower, state.unit, ui)} - ${formatLength(upper, state.unit, ui)}`;
|
|
293
|
+
elements.formula.textContent = state.sex === 'boy' ? ui.formulaBoy : ui.formulaGirl;
|
|
294
|
+
elements.child.setAttribute('data-sex', state.sex);
|
|
295
|
+
elements.motherFigure.setAttribute('transform', `translate(168 366) scale(${motherScale})`);
|
|
296
|
+
elements.child.setAttribute('transform', `translate(386 366) scale(${childScale})`);
|
|
297
|
+
elements.fatherFigure.setAttribute('transform', `translate(600 366) scale(${fatherScale})`);
|
|
298
|
+
elements.targetLine.setAttribute('y1', String(targetY));
|
|
299
|
+
elements.targetLine.setAttribute('y2', String(targetY));
|
|
300
|
+
elements.rangeBand.setAttribute('d', `M110 ${upperY} C260 ${upperY - 22} 430 ${upperY - 22} 650 ${upperY} L650 ${lowerY} C430 ${lowerY + 22} 260 ${lowerY + 22} 110 ${lowerY} Z`);
|
|
301
|
+
renderTicks(elements, state, ui);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function initTargetHeight(container: Element) {
|
|
305
|
+
const ui = JSON.parse((container as HTMLElement).dataset.ui || '{}') as Record<string, string>;
|
|
306
|
+
const elements = getElements(container);
|
|
307
|
+
if (!elements) return;
|
|
308
|
+
const state = loadState();
|
|
309
|
+
const update = () => {
|
|
310
|
+
state.motherCm = state.unit === 'imperial' ? inToCm(Number(elements.mother.value)) : Number(elements.mother.value);
|
|
311
|
+
state.fatherCm = state.unit === 'imperial' ? inToCm(Number(elements.father.value)) : Number(elements.father.value);
|
|
312
|
+
render(elements, state, ui);
|
|
313
|
+
saveState(state);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
elements.unitBtns.forEach((btn) => btn.addEventListener('click', () => {
|
|
317
|
+
state.unit = btn.dataset.unit as Unit;
|
|
318
|
+
syncButtons(elements, state);
|
|
319
|
+
syncRanges(elements, state);
|
|
320
|
+
update();
|
|
321
|
+
}));
|
|
322
|
+
elements.sexBtns.forEach((btn) => btn.addEventListener('click', () => {
|
|
323
|
+
state.sex = btn.dataset.sex as Sex;
|
|
324
|
+
syncButtons(elements, state);
|
|
325
|
+
update();
|
|
326
|
+
}));
|
|
327
|
+
elements.mother.addEventListener('input', update);
|
|
328
|
+
elements.father.addEventListener('input', update);
|
|
329
|
+
syncButtons(elements, state);
|
|
330
|
+
syncRanges(elements, state);
|
|
331
|
+
update();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
document.querySelectorAll('.the').forEach(initTargetHeight);
|
|
335
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { HealthToolEntry } from '../../types';
|
|
2
|
+
import type { TargetHeightEstimatorUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export const targetHeightEstimator: HealthToolEntry<TargetHeightEstimatorUI> = {
|
|
5
|
+
id: 'targetHeightEstimator',
|
|
6
|
+
icons: {
|
|
7
|
+
bg: 'mdi:human-male-height',
|
|
8
|
+
fg: 'mdi:account-child',
|
|
9
|
+
},
|
|
10
|
+
i18n: {
|
|
11
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
12
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
13
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
14
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
15
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
16
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
17
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
18
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
19
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
20
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
21
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
22
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
23
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
24
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
25
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
26
|
+
},
|
|
27
|
+
};
|