| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
function calcMaxHR(age, formula) { |
| 5 |
if (formula === 'tanaka') return Math.round(208 - 0.7 * age); |
| 6 |
if (formula === 'gellish') return Math.round(207 - 0.7 * age); |
| 7 |
return 220 - age; // fox (default) |
| 8 |
} |
| 9 |
|
| 10 |
var ZONES = [ |
| 11 |
{ num: 1, name: 'Recovery', icon: '🚶', pctLo: 50, pctHi: 60, purpose: 'Rest & recovery — improve base fitness and promote active recovery' }, |
| 12 |
{ num: 2, name: 'Fat Burn', icon: '🔥', pctLo: 60, pctHi: 70, purpose: 'Fat burning & aerobic base — improve endurance and metabolic efficiency' }, |
| 13 |
{ num: 3, name: 'Aerobic', icon: '🏃', pctLo: 70, pctHi: 80, purpose: 'Improve cardiovascular efficiency and increase aerobic capacity' }, |
| 14 |
{ num: 4, name: 'Threshold', icon: '⚡', pctLo: 80, pctHi: 90, purpose: 'Increase lactate threshold — improve speed and race performance' }, |
| 15 |
{ num: 5, name: 'Max Effort', icon: '🚀', pctLo: 90, pctHi: 100, purpose: 'Maximum effort — develop peak power and neuromuscular speed' } |
| 16 |
]; |
| 17 |
|
| 18 |
var FORMULA_LABELS = { |
| 19 |
fox: 'Fox (220 − age)', |
| 20 |
tanaka: 'Tanaka (208 − 0.7×age)', |
| 21 |
gellish: 'Gellish (207 − 0.7×age)' |
| 22 |
}; |
| 23 |
|
| 24 |
function typoCssVarsForEl(typo, prefix, el) { |
| 25 |
if (!typo || typeof typo !== 'object') return; |
| 26 |
var map = { |
| 27 |
family:'font-family', weight:'font-weight', style:'font-style', |
| 28 |
decoration:'text-decoration', transform:'text-transform', |
| 29 |
sizeDesktop:'font-size-d', sizeTablet:'font-size-t', sizeMobile:'font-size-m', |
| 30 |
lineHeightDesktop:'line-height-d', lineHeightTablet:'line-height-t', lineHeightMobile:'line-height-m', |
| 31 |
letterSpacingDesktop:'letter-spacing-d', letterSpacingTablet:'letter-spacing-t', letterSpacingMobile:'letter-spacing-m', |
| 32 |
wordSpacingDesktop:'word-spacing-d', wordSpacingTablet:'word-spacing-t', wordSpacingMobile:'word-spacing-m' |
| 33 |
}; |
| 34 |
Object.keys(map).forEach(function(k) { |
| 35 |
if (typo[k] !== undefined && typo[k] !== '') { |
| 36 |
var v = typo[k]; |
| 37 |
if (['sizeDesktop','sizeTablet','sizeMobile'].indexOf(k) !== -1) v = v + (typo.sizeUnit || 'px'); |
| 38 |
else if (['lineHeightDesktop','lineHeightTablet','lineHeightMobile'].indexOf(k) !== -1) v = v + (typo.lineHeightUnit || ''); |
| 39 |
else if (['letterSpacingDesktop','letterSpacingTablet','letterSpacingMobile'].indexOf(k) !== -1) v = v + (typo.letterSpacingUnit || 'px'); |
| 40 |
else if (['wordSpacingDesktop','wordSpacingTablet','wordSpacingMobile'].indexOf(k) !== -1) v = v + (typo.wordSpacingUnit || 'px'); |
| 41 |
el.style.setProperty(prefix + map[k], String(v)); |
| 42 |
} |
| 43 |
}); |
| 44 |
} |
| 45 |
|
| 46 |
document.querySelectorAll('.bkbg-hrc-app').forEach(function (root) { |
| 47 |
var opts = {}; |
| 48 |
try { opts = JSON.parse(root.getAttribute('data-opts') || '{}'); } catch(e) {} |
| 49 |
|
| 50 |
var defaultAge = parseInt(opts.defaultAge) || 30; |
| 51 |
var defaultFormula = opts.defaultFormula || 'fox'; |
| 52 |
var showIcon = opts.showZoneIcon !== false; |
| 53 |
var showPurpose = opts.showPurpose !== false; |
| 54 |
|
| 55 |
var accentColor = opts.accentColor || '#ef4444'; |
| 56 |
var sectionBg = opts.sectionBg || '#fff7f7'; |
| 57 |
var cardBg = opts.cardBg || '#ffffff'; |
| 58 |
var inputBg = opts.inputBg || '#f9fafb'; |
| 59 |
var labelColor = opts.labelColor || '#374151'; |
| 60 |
var titleColor = opts.titleColor || '#111827'; |
| 61 |
var subtitleColor = opts.subtitleColor || '#6b7280'; |
| 62 |
var zColors = [ |
| 63 |
opts.z1Color || '#6ee7b7', |
| 64 |
opts.z2Color || '#34d399', |
| 65 |
opts.z3Color || '#fbbf24', |
| 66 |
opts.z4Color || '#f97316', |
| 67 |
opts.z5Color || '#ef4444' |
| 68 |
]; |
| 69 |
|
| 70 |
var currentAge = defaultAge; |
| 71 |
var currentFormula = defaultFormula; |
| 72 |
|
| 73 |
typoCssVarsForEl(opts.typoTitle, '--bkbg-hrc-tt-', root); |
| 74 |
typoCssVarsForEl(opts.typoSubtitle, '--bkbg-hrc-st-', root); |
| 75 |
|
| 76 |
root.innerHTML = |
| 77 |
'<div class="bkbg-hrc-card" style="background:' + cardBg + ';max-width:' + (opts.contentMaxWidth || 760) + 'px;">' + |
| 78 |
'<h2 class="bkbg-hrc-title" style="color:' + titleColor + ';">' + (opts.title || 'Heart Rate Zone Calculator') + '</h2>' + |
| 79 |
'<p class="bkbg-hrc-subtitle" style="color:' + subtitleColor + ';">' + (opts.subtitle || 'Find your personalized training zones based on your age') + '</p>' + |
| 80 |
|
| 81 |
'<div class="bkbg-hrc-controls" style="background:' + sectionBg + ';">' + |
| 82 |
'<div class="bkbg-hrc-control-group">' + |
| 83 |
'<label class="bkbg-hrc-label" style="color:' + labelColor + ';">🎂 Your Age: <strong id="hrc-age-display" style="color:' + accentColor + ';">' + defaultAge + '</strong></label>' + |
| 84 |
'<input type="range" class="bkbg-hrc-slider" id="hrc-age-slider" min="18" max="80" value="' + defaultAge + '" style="accent-color:' + accentColor + ';">' + |
| 85 |
'</div>' + |
| 86 |
'<div class="bkbg-hrc-control-group">' + |
| 87 |
'<label class="bkbg-hrc-label" style="color:' + labelColor + ';">📐 Formula</label>' + |
| 88 |
'<select class="bkbg-hrc-select" id="hrc-formula" style="background:' + inputBg + ';">' + |
| 89 |
'<option value="fox"' + (defaultFormula === 'fox' ? ' selected' : '') + '>Fox: 220 − age</option>' + |
| 90 |
'<option value="tanaka"' + (defaultFormula === 'tanaka' ? ' selected' : '') + '>Tanaka: 208 − 0.7×age</option>' + |
| 91 |
'<option value="gellish"' + (defaultFormula === 'gellish' ? ' selected' : '') + '>Gellish: 207 − 0.7×age</option>' + |
| 92 |
'</select>' + |
| 93 |
'</div>' + |
| 94 |
'</div>' + |
| 95 |
|
| 96 |
'<div class="bkbg-hrc-maxhr" id="hrc-maxhr-box" style="border-color:' + accentColor + ';background:' + accentColor + '12;">' + |
| 97 |
'<div class="bkbg-hrc-maxhr-label" style="color:' + labelColor + ';">❤️ Maximum Heart Rate</div>' + |
| 98 |
'<div class="bkbg-hrc-maxhr-val" id="hrc-maxhr-val" style="color:' + accentColor + ';">' + calcMaxHR(defaultAge, defaultFormula) + '</div>' + |
| 99 |
'<div class="bkbg-hrc-maxhr-unit" style="color:' + subtitleColor + ';">beats per minute</div>' + |
| 100 |
'</div>' + |
| 101 |
|
| 102 |
'<div class="bkbg-hrc-zones" id="hrc-zones"></div>' + |
| 103 |
'</div>'; |
| 104 |
|
| 105 |
var ageSlider = root.querySelector('#hrc-age-slider'); |
| 106 |
var ageDisplay = root.querySelector('#hrc-age-display'); |
| 107 |
var fmlSelect = root.querySelector('#hrc-formula'); |
| 108 |
var maxhrVal = root.querySelector('#hrc-maxhr-val'); |
| 109 |
var zonesEl = root.querySelector('#hrc-zones'); |
| 110 |
|
| 111 |
function renderZones() { |
| 112 |
var maxHR = calcMaxHR(currentAge, currentFormula); |
| 113 |
maxhrVal.textContent = maxHR; |
| 114 |
|
| 115 |
var html = ''; |
| 116 |
ZONES.forEach(function(z, i) { |
| 117 |
var color = zColors[i]; |
| 118 |
var lo = Math.round(maxHR * z.pctLo / 100); |
| 119 |
var hi = Math.round(maxHR * z.pctHi / 100); |
| 120 |
html += |
| 121 |
'<div class="bkbg-hrc-zone-row" style="border-left-color:' + color + ';background:' + color + '18;">' + |
| 122 |
'<div class="bkbg-hrc-zone-left">' + |
| 123 |
'<div class="bkbg-hrc-zone-badge" style="background:' + color + ';color:#fff;">' + |
| 124 |
(showIcon ? z.icon + ' ' : '') + 'Zone ' + z.num + |
| 125 |
'</div>' + |
| 126 |
'<div class="bkbg-hrc-zone-name">' + z.name + '</div>' + |
| 127 |
'</div>' + |
| 128 |
'<div class="bkbg-hrc-zone-center">' + |
| 129 |
(showPurpose ? '<div class="bkbg-hrc-zone-purpose">' + z.purpose + '</div>' : '') + |
| 130 |
'</div>' + |
| 131 |
'<div class="bkbg-hrc-zone-right">' + |
| 132 |
'<div class="bkbg-hrc-zone-bpm" style="color:' + color + ';">' + lo + ' – ' + hi + '</div>' + |
| 133 |
'<div class="bkbg-hrc-zone-pct">' + z.pctLo + '–' + z.pctHi + '% max HR</div>' + |
| 134 |
'</div>' + |
| 135 |
'</div>'; |
| 136 |
}); |
| 137 |
zonesEl.innerHTML = html; |
| 138 |
} |
| 139 |
|
| 140 |
ageSlider.addEventListener('input', function () { |
| 141 |
currentAge = parseInt(this.value); |
| 142 |
ageDisplay.textContent = currentAge; |
| 143 |
renderZones(); |
| 144 |
}); |
| 145 |
|
| 146 |
fmlSelect.addEventListener('change', function () { |
| 147 |
currentFormula = this.value; |
| 148 |
renderZones(); |
| 149 |
}); |
| 150 |
|
| 151 |
renderZones(); |
| 152 |
}); |
| 153 |
|
| 154 |
})(); |
| 155 |
|