| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
var _typoKeys = { |
| 5 |
family:'font-family', weight:'font-weight', style:'font-style', |
| 6 |
decoration:'text-decoration', transform:'text-transform', |
| 7 |
sizeDesktop:'font-size-d', sizeTablet:'font-size-t', sizeMobile:'font-size-m', |
| 8 |
lineHeightDesktop:'line-height-d', lineHeightTablet:'line-height-t', lineHeightMobile:'line-height-m', |
| 9 |
letterSpacingDesktop:'letter-spacing-d', letterSpacingTablet:'letter-spacing-t', letterSpacingMobile:'letter-spacing-m', |
| 10 |
wordSpacingDesktop:'word-spacing-d', wordSpacingTablet:'word-spacing-t', wordSpacingMobile:'word-spacing-m' |
| 11 |
}; |
| 12 |
function typoCssVarsForEl(el, obj, prefix) { |
| 13 |
if (!obj || typeof obj !== 'object') return; |
| 14 |
Object.keys(_typoKeys).forEach(function (k) { |
| 15 |
var v = obj[k]; |
| 16 |
if (v === undefined || v === '' || v === null) return; |
| 17 |
if (k === 'sizeDesktop' || k === 'sizeTablet' || k === 'sizeMobile') v = v + (obj.sizeUnit || 'px'); |
| 18 |
else if (k === 'lineHeightDesktop' || k === 'lineHeightTablet' || k === 'lineHeightMobile') v = v + (obj.lineHeightUnit || ''); |
| 19 |
else if (k === 'letterSpacingDesktop' || k === 'letterSpacingTablet' || k === 'letterSpacingMobile') v = v + (obj.letterSpacingUnit || 'px'); |
| 20 |
else if (k === 'wordSpacingDesktop' || k === 'wordSpacingTablet' || k === 'wordSpacingMobile') v = v + (obj.wordSpacingUnit || 'px'); |
| 21 |
el.style.setProperty(prefix + _typoKeys[k], String(v)); |
| 22 |
}); |
| 23 |
} |
| 24 |
|
| 25 |
var KM_PER_MILE = 1.60934; |
| 26 |
|
| 27 |
function pad2(n) { return String(Math.floor(Math.max(0, n))).padStart(2, '0'); } |
| 28 |
|
| 29 |
function secsToHMS(s) { |
| 30 |
s = Math.round(Math.max(0, s)); |
| 31 |
var h = Math.floor(s / 3600); |
| 32 |
var m = Math.floor((s % 3600) / 60); |
| 33 |
var sec = s % 60; |
| 34 |
return (h > 0 ? h + ':' + pad2(m) : m) + ':' + pad2(sec); |
| 35 |
} |
| 36 |
|
| 37 |
function secsToPace(s) { |
| 38 |
s = Math.max(0, s); |
| 39 |
var m = Math.floor(s / 60); |
| 40 |
var sec = Math.round(s % 60); |
| 41 |
if (sec === 60) { m++; sec = 0; } |
| 42 |
return m + ':' + pad2(sec); |
| 43 |
} |
| 44 |
|
| 45 |
function paceToSecs(str) { |
| 46 |
str = String(str || '').trim(); |
| 47 |
var parts = str.split(':'); |
| 48 |
if (parts.length >= 2) return parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10); |
| 49 |
return parseFloat(str) * 60 || 0; |
| 50 |
} |
| 51 |
|
| 52 |
function timeToSecs(str) { |
| 53 |
str = String(str || '').trim(); |
| 54 |
var parts = str.split(':'); |
| 55 |
if (parts.length === 3) return parseInt(parts[0],10)*3600 + parseInt(parts[1],10)*60 + parseInt(parts[2],10); |
| 56 |
if (parts.length === 2) return parseInt(parts[0],10)*60 + parseInt(parts[1],10); |
| 57 |
return parseFloat(str) * 60 || 0; |
| 58 |
} |
| 59 |
|
| 60 |
function initPaceCalc(app) { |
| 61 |
var opts = {}; |
| 62 |
try { opts = JSON.parse(app.getAttribute('data-opts') || '{}'); } catch(e) {} |
| 63 |
|
| 64 |
var accent = opts.accentColor || '#6c3fb5'; |
| 65 |
var cardBg = opts.cardBg || '#ffffff'; |
| 66 |
var resultBg = opts.resultBg || '#f5f3ff'; |
| 67 |
var resultBorder= opts.resultBorder || '#ede9fe'; |
| 68 |
var resultColor = opts.resultColor || '#6c3fb5'; |
| 69 |
var presetBg = opts.presetBg || '#f3f4f6'; |
| 70 |
var presetColor = opts.presetColor || '#374151'; |
| 71 |
var convBg = opts.convBg || '#f9fafb'; |
| 72 |
var labelColor = opts.labelColor || '#374151'; |
| 73 |
var titleColor = opts.titleColor || '#1f2937'; |
| 74 |
var subtitleColor = opts.subtitleColor || '#6b7280'; |
| 75 |
var sectionBg = opts.sectionBg || ''; |
| 76 |
var maxWidth = opts.maxWidth || 560; |
| 77 |
var cardRadius = opts.cardRadius || 16; |
| 78 |
var inputRadius = opts.inputRadius || 8; |
| 79 |
var titleSize = opts.titleSize || 28; |
| 80 |
var resultSize = opts.resultSize || 52; |
| 81 |
var padTop = opts.paddingTop || 60; |
| 82 |
var padBot = opts.paddingBottom || 60; |
| 83 |
var showTitle = opts.showTitle !== false; |
| 84 |
var showSub = opts.showSubtitle !== false; |
| 85 |
var showPresets = opts.showPresets !== false; |
| 86 |
var showSpeedConv = opts.showSpeedConv !== false; |
| 87 |
var defMode = opts.defaultMode || 'find_pace'; |
| 88 |
var defUnit = opts.defaultUnit || 'km'; |
| 89 |
var presets = opts.presets || [{ label:'5K', dist:5 }, { label:'10K', dist:10 }, { label:'Half', dist:21.0975 }, { label:'Marathon', dist:42.195 }]; |
| 90 |
|
| 91 |
var mode = defMode; |
| 92 |
var unit = defUnit; |
| 93 |
var distVal = '10'; |
| 94 |
var timeVal = '1:00:00'; |
| 95 |
var paceVal = '6:00'; |
| 96 |
|
| 97 |
// Build DOM |
| 98 |
app.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; |
| 99 |
if (sectionBg) app.style.background = sectionBg; |
| 100 |
app.style.paddingTop = padTop + 'px'; |
| 101 |
app.style.paddingBottom = padBot + 'px'; |
| 102 |
|
| 103 |
typoCssVarsForEl(app, opts.titleTypo, '--bkbg-pace-tt-'); |
| 104 |
typoCssVarsForEl(app, opts.resultTypo, '--bkbg-pace-rs-'); |
| 105 |
|
| 106 |
var card = document.createElement('div'); |
| 107 |
card.className = 'bkbg-pace-card'; |
| 108 |
card.style.cssText = 'background:' + cardBg + ';border-radius:' + cardRadius + 'px;padding:36px 32px;max-width:' + maxWidth + 'px;margin:0 auto;'; |
| 109 |
app.appendChild(card); |
| 110 |
|
| 111 |
// Title |
| 112 |
if (showTitle && opts.title) { |
| 113 |
var titleEl = document.createElement('div'); |
| 114 |
titleEl.className = 'bkbg-pace-title'; |
| 115 |
titleEl.style.color = titleColor; |
| 116 |
titleEl.style.marginBottom = '6px'; |
| 117 |
titleEl.textContent = opts.title; |
| 118 |
card.appendChild(titleEl); |
| 119 |
} |
| 120 |
if (showSub && opts.subtitle) { |
| 121 |
var subEl = document.createElement('div'); |
| 122 |
subEl.style.cssText = 'font-size:15px;color:' + subtitleColor + ';margin-bottom:20px;'; |
| 123 |
subEl.textContent = opts.subtitle; |
| 124 |
card.appendChild(subEl); |
| 125 |
} |
| 126 |
|
| 127 |
var modes = [ |
| 128 |
{ key: 'find_pace', label: 'Find Pace' }, |
| 129 |
{ key: 'find_time', label: 'Find Time' }, |
| 130 |
{ key: 'find_dist', label: 'Find Distance' } |
| 131 |
]; |
| 132 |
var units = [{ key: 'km', label: 'Kilometers' }, { key: 'mi', label: 'Miles' }]; |
| 133 |
|
| 134 |
// Mode tabs |
| 135 |
var modeTabWrap = document.createElement('div'); |
| 136 |
modeTabWrap.style.cssText = 'display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;'; |
| 137 |
card.appendChild(modeTabWrap); |
| 138 |
|
| 139 |
var modeTabBtns = {}; |
| 140 |
modes.forEach(function(m) { |
| 141 |
var btn = document.createElement('button'); |
| 142 |
btn.className = 'bkbg-pace-tab-btn'; |
| 143 |
btn.textContent = m.label; |
| 144 |
btn.dataset.key = m.key; |
| 145 |
modeTabBtns[m.key] = btn; |
| 146 |
btn.addEventListener('click', function() { mode = m.key; refreshMode(); refreshResult(); }); |
| 147 |
modeTabWrap.appendChild(btn); |
| 148 |
}); |
| 149 |
|
| 150 |
// Unit tabs |
| 151 |
var unitTabWrap = document.createElement('div'); |
| 152 |
unitTabWrap.style.cssText = 'display:flex;gap:8px;margin-bottom:18px;'; |
| 153 |
card.appendChild(unitTabWrap); |
| 154 |
|
| 155 |
var unitTabBtns = {}; |
| 156 |
units.forEach(function(u) { |
| 157 |
var btn = document.createElement('button'); |
| 158 |
btn.className = 'bkbg-pace-tab-btn'; |
| 159 |
btn.textContent = u.label; |
| 160 |
btn.dataset.key = u.key; |
| 161 |
unitTabBtns[u.key] = btn; |
| 162 |
btn.addEventListener('click', function() { unit = u.key; refreshUnit(); refreshResult(); }); |
| 163 |
unitTabWrap.appendChild(btn); |
| 164 |
}); |
| 165 |
|
| 166 |
// Presets |
| 167 |
var presetWrap = document.createElement('div'); |
| 168 |
presetWrap.style.cssText = 'display:flex;gap:8px;flex-wrap:wrap;margin-bottom:18px;'; |
| 169 |
card.appendChild(presetWrap); |
| 170 |
|
| 171 |
function buildPresets() { |
| 172 |
presetWrap.innerHTML = ''; |
| 173 |
if (!showPresets || mode === 'find_dist') { presetWrap.style.display = 'none'; return; } |
| 174 |
presetWrap.style.display = 'flex'; |
| 175 |
presets.forEach(function(p) { |
| 176 |
var d = unit === 'km' ? p.dist : p.dist / KM_PER_MILE; |
| 177 |
var btn = document.createElement('button'); |
| 178 |
btn.className = 'bkbg-pace-preset-btn'; |
| 179 |
btn.style.background = presetBg; |
| 180 |
btn.style.color = presetColor; |
| 181 |
btn.textContent = p.label; |
| 182 |
btn.addEventListener('click', function() { |
| 183 |
distVal = String(parseFloat(d.toFixed(3))); |
| 184 |
distInput.value = distVal; |
| 185 |
refreshResult(); |
| 186 |
}); |
| 187 |
presetWrap.appendChild(btn); |
| 188 |
}); |
| 189 |
} |
| 190 |
|
| 191 |
// Input grid |
| 192 |
var inputGrid = document.createElement('div'); |
| 193 |
inputGrid.className = 'bkbg-pace-input-grid'; |
| 194 |
card.appendChild(inputGrid); |
| 195 |
|
| 196 |
function makeInputRow(label, hint, initVal, onChange) { |
| 197 |
var wrap = document.createElement('div'); |
| 198 |
wrap.className = 'bkbg-pace-input-row'; |
| 199 |
var lbl = document.createElement('label'); |
| 200 |
lbl.className = 'bkbg-pace-label'; |
| 201 |
lbl.style.color = labelColor; |
| 202 |
var lspan = document.createElement('span'); lspan.textContent = label; |
| 203 |
var hspan = document.createElement('span'); hspan.className = 'bkbg-pace-label-hint'; hspan.textContent = hint; |
| 204 |
lbl.appendChild(lspan); lbl.appendChild(hspan); |
| 205 |
var inp = document.createElement('input'); |
| 206 |
inp.className = 'bkbg-pace-input'; |
| 207 |
inp.type = 'text'; |
| 208 |
inp.value = initVal; |
| 209 |
inp.style.borderRadius = inputRadius + 'px'; |
| 210 |
inp.addEventListener('input', function() { onChange(inp.value); refreshResult(); }); |
| 211 |
wrap.appendChild(lbl); wrap.appendChild(inp); |
| 212 |
return { el: wrap, inp: inp, hspan: hspan, lspan: lspan }; |
| 213 |
} |
| 214 |
|
| 215 |
var distRow = makeInputRow('Distance', unit === 'km' ? 'km' : 'mi', distVal, function(v) { distVal = v; }); |
| 216 |
var timeRow = makeInputRow('Time', 'H:MM:SS', timeVal, function(v) { timeVal = v; }); |
| 217 |
var paceRow = makeInputRow('Pace', 'M:SS/' + (unit==='km'?'km':'mi'), paceVal, function(v) { paceVal = v; }); |
| 218 |
|
| 219 |
var distInput = distRow.inp; |
| 220 |
var timeInput = timeRow.inp; |
| 221 |
var paceInput = paceRow.inp; |
| 222 |
|
| 223 |
inputGrid.appendChild(distRow.el); |
| 224 |
inputGrid.appendChild(timeRow.el); |
| 225 |
inputGrid.appendChild(paceRow.el); |
| 226 |
|
| 227 |
// Result area |
| 228 |
var resultBox = document.createElement('div'); |
| 229 |
resultBox.className = 'bkbg-pace-result'; |
| 230 |
resultBox.style.cssText = 'background:' + resultBg + ';border:2px solid ' + resultBorder + ';border-radius:' + cardRadius + 'px;padding:24px 28px;'; |
| 231 |
card.appendChild(resultBox); |
| 232 |
|
| 233 |
// Speed conversion |
| 234 |
var convBox = document.createElement('div'); |
| 235 |
convBox.className = 'bkbg-pace-conv'; |
| 236 |
convBox.style.background = convBg; |
| 237 |
card.appendChild(convBox); |
| 238 |
|
| 239 |
function refreshMode() { |
| 240 |
Object.keys(modeTabBtns).forEach(function(k) { |
| 241 |
var btn = modeTabBtns[k]; |
| 242 |
var active = k === mode; |
| 243 |
btn.style.background = active ? accent : 'transparent'; |
| 244 |
btn.style.color = active ? '#fff' : accent; |
| 245 |
btn.style.border = '2px solid ' + (active ? accent : '#e5e7eb'); |
| 246 |
}); |
| 247 |
distRow.el.style.display = 'block'; |
| 248 |
timeRow.el.style.display = (mode === 'find_dist' || mode === 'find_pace') ? 'block' : 'block'; |
| 249 |
paceRow.el.style.display = 'block'; |
| 250 |
|
| 251 |
// update grid: for find_dist remove dist, show time+pace |
| 252 |
if (mode === 'find_dist') { |
| 253 |
distRow.el.style.display = 'none'; |
| 254 |
timeRow.el.style.display = 'block'; |
| 255 |
paceRow.el.style.display = 'block'; |
| 256 |
timeRow.hspan.textContent = 'H:MM:SS'; |
| 257 |
} else if (mode === 'find_pace') { |
| 258 |
distRow.el.style.display = 'block'; |
| 259 |
timeRow.el.style.display = 'block'; |
| 260 |
paceRow.el.style.display = 'none'; |
| 261 |
distRow.hspan.textContent = unit === 'km' ? 'km' : 'mi'; |
| 262 |
timeRow.hspan.textContent = 'H:MM:SS'; |
| 263 |
} else { // find_time |
| 264 |
distRow.el.style.display = 'block'; |
| 265 |
timeRow.el.style.display = 'none'; |
| 266 |
paceRow.el.style.display = 'block'; |
| 267 |
distRow.hspan.textContent = unit === 'km' ? 'km' : 'mi'; |
| 268 |
paceRow.hspan.textContent = 'M:SS/' + (unit === 'km' ? 'km' : 'mi'); |
| 269 |
} |
| 270 |
buildPresets(); |
| 271 |
} |
| 272 |
|
| 273 |
function refreshUnit() { |
| 274 |
Object.keys(unitTabBtns).forEach(function(k) { |
| 275 |
var btn = unitTabBtns[k]; |
| 276 |
var active = k === unit; |
| 277 |
btn.style.background = active ? accent : 'transparent'; |
| 278 |
btn.style.color = active ? '#fff' : accent; |
| 279 |
btn.style.border = '2px solid ' + (active ? accent : '#e5e7eb'); |
| 280 |
}); |
| 281 |
distRow.hspan.textContent = unit === 'km' ? 'km' : 'mi'; |
| 282 |
paceRow.hspan.textContent = 'M:SS/' + (unit === 'km' ? 'km' : 'mi'); |
| 283 |
buildPresets(); |
| 284 |
} |
| 285 |
|
| 286 |
function refreshResult() { |
| 287 |
var distN = parseFloat(distVal) || 0; |
| 288 |
var timeSec = timeToSecs(timeVal); |
| 289 |
var paceSec = paceToSecs(paceVal); |
| 290 |
var pu = unit === 'km' ? 'km' : 'mi'; |
| 291 |
|
| 292 |
var labelTxt, mainTxt, subTxt = '', extraTxt = ''; |
| 293 |
var speedKmh, speedMph, speedMs; |
| 294 |
|
| 295 |
if (mode === 'find_pace') { |
| 296 |
if (distN <= 0 || timeSec <= 0) { resultBox.innerHTML = '<div style="color:' + labelColor + '">Enter distance and time to calculate</div>'; convBox.innerHTML = ''; return; } |
| 297 |
var ps = timeSec / distN; |
| 298 |
speedKmh = 3600 / (unit === 'km' ? ps : ps * KM_PER_MILE); |
| 299 |
labelTxt = 'Your Pace'; mainTxt = secsToPace(ps); subTxt = '/' + pu; |
| 300 |
} else if (mode === 'find_time') { |
| 301 |
if (distN <= 0 || paceSec <= 0) { resultBox.innerHTML = '<div style="color:' + labelColor + '">Enter distance and pace to calculate</div>'; convBox.innerHTML = ''; return; } |
| 302 |
var ts = distN * paceSec; |
| 303 |
speedKmh = 3600 / (unit === 'km' ? paceSec : paceSec * KM_PER_MILE); |
| 304 |
labelTxt = 'Finish Time'; mainTxt = secsToHMS(ts); |
| 305 |
} else { |
| 306 |
// find_dist |
| 307 |
if (timeSec <= 0 || paceSec <= 0) { resultBox.innerHTML = '<div style="color:' + labelColor + '">Enter time and pace to calculate</div>'; convBox.innerHTML = ''; return; } |
| 308 |
var d = timeSec / paceSec; |
| 309 |
speedKmh = 3600 / (unit === 'km' ? paceSec : paceSec * KM_PER_MILE); |
| 310 |
labelTxt = 'Distance Covered'; mainTxt = d.toFixed(2); subTxt = pu; |
| 311 |
} |
| 312 |
|
| 313 |
resultBox.innerHTML = ''; |
| 314 |
var lbl = document.createElement('div'); |
| 315 |
lbl.style.cssText = 'font-size:13px;font-weight:600;color:' + labelColor + ';margin-bottom:6px;'; |
| 316 |
lbl.textContent = labelTxt; |
| 317 |
resultBox.appendChild(lbl); |
| 318 |
var mainRow = document.createElement('div'); |
| 319 |
mainRow.style.cssText = 'display:flex;align-items:baseline;justify-content:center;gap:4px;'; |
| 320 |
var mainSpan = document.createElement('span'); |
| 321 |
mainSpan.className = 'bkbg-pace-result-value'; |
| 322 |
mainSpan.style.color = resultColor; |
| 323 |
mainSpan.textContent = mainTxt; |
| 324 |
mainRow.appendChild(mainSpan); |
| 325 |
if (subTxt) { |
| 326 |
var unitSpan = document.createElement('span'); |
| 327 |
unitSpan.style.cssText = 'font-size:22px;font-weight:600;color:' + labelColor + ';'; |
| 328 |
unitSpan.textContent = ' ' + subTxt; |
| 329 |
mainRow.appendChild(unitSpan); |
| 330 |
} |
| 331 |
resultBox.appendChild(mainRow); |
| 332 |
|
| 333 |
// Speed conversions |
| 334 |
if (showSpeedConv && speedKmh) { |
| 335 |
speedMph = speedKmh / KM_PER_MILE; |
| 336 |
speedMs = speedKmh / 3.6; |
| 337 |
convBox.innerHTML = ''; |
| 338 |
convBox.style.display = 'flex'; |
| 339 |
[ |
| 340 |
{ val: speedKmh.toFixed(2), lbl: 'km/h' }, |
| 341 |
{ val: speedMph.toFixed(2), lbl: 'mph' }, |
| 342 |
{ val: speedMs.toFixed(2), lbl: 'm/s' } |
| 343 |
].forEach(function(item) { |
| 344 |
var div = document.createElement('div'); |
| 345 |
div.className = 'bkbg-pace-conv-item'; |
| 346 |
div.style.color = labelColor; |
| 347 |
div.innerHTML = '<span class="bkbg-pace-conv-val">' + item.val + '</span><span class="bkbg-pace-conv-lbl">' + item.lbl + '</span>'; |
| 348 |
convBox.appendChild(div); |
| 349 |
}); |
| 350 |
} else { |
| 351 |
convBox.innerHTML = ''; |
| 352 |
convBox.style.display = 'none'; |
| 353 |
} |
| 354 |
} |
| 355 |
|
| 356 |
// Init |
| 357 |
refreshMode(); |
| 358 |
refreshUnit(); |
| 359 |
refreshResult(); |
| 360 |
} |
| 361 |
|
| 362 |
document.querySelectorAll('.bkbg-pace-app').forEach(initPaceCalc); |
| 363 |
})(); |
| 364 |
|