| 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 |
['sizeUnit','font-size-unit'], |
| 9 |
['lineHeightDesktop','line-height-d'],['lineHeightTablet','line-height-t'],['lineHeightMobile','line-height-m'], |
| 10 |
['letterSpacingDesktop','letter-spacing-d'],['letterSpacingTablet','letter-spacing-t'],['letterSpacingMobile','letter-spacing-m'], |
| 11 |
['wordSpacingDesktop','word-spacing-d'],['wordSpacingTablet','word-spacing-t'],['wordSpacingMobile','word-spacing-m'] |
| 12 |
]; |
| 13 |
function typoCssVarsForEl(el, typo, prefix) { |
| 14 |
if (!typo || typeof typo !== 'object') return; |
| 15 |
_typoKeys.forEach(function(pair) { |
| 16 |
var val = typo[pair[0]]; |
| 17 |
if (val !== undefined && val !== '' && val !== null) { |
| 18 |
var v = (typeof val === 'number' && pair[1].indexOf('font-size') !== -1) ? val + 'px' : String(val); |
| 19 |
el.style.setProperty(prefix + pair[1], v); |
| 20 |
} |
| 21 |
}); |
| 22 |
} |
| 23 |
|
| 24 |
var CYCLE_MIN = 90; |
| 25 |
|
| 26 |
function pad2(n) { return String(n).padStart(2, '0'); } |
| 27 |
|
| 28 |
function addMinutes(h, m, mins) { |
| 29 |
var total = h * 60 + m + mins; |
| 30 |
total = ((total % 1440) + 1440) % 1440; |
| 31 |
return { h: Math.floor(total / 60), m: total % 60 }; |
| 32 |
} |
| 33 |
function subMinutes(h, m, mins) { |
| 34 |
return addMinutes(h, m, -mins); |
| 35 |
} |
| 36 |
function fmt12(h, m) { |
| 37 |
var ampm = h < 12 ? 'AM' : 'PM'; |
| 38 |
var hh = h % 12 || 12; |
| 39 |
return hh + ':' + pad2(m) + ' ' + ampm; |
| 40 |
} |
| 41 |
|
| 42 |
function calcTimes(mode, h, m, onsetDelay, cMin, cMax) { |
| 43 |
var results = []; |
| 44 |
for (var c = cMax; c >= cMin; c--) { |
| 45 |
var totalMins = (onsetDelay || 14) + c * CYCLE_MIN; |
| 46 |
var t = mode === 'wake' ? addMinutes(h, m, totalMins) : subMinutes(h, m, totalMins); |
| 47 |
results.push({ cycles: c, h: t.h, m: t.m, totalSleep: c * CYCLE_MIN }); |
| 48 |
} |
| 49 |
return results; |
| 50 |
} |
| 51 |
|
| 52 |
function buildApp(app) { |
| 53 |
var opts = {}; |
| 54 |
try { opts = JSON.parse(app.getAttribute('data-opts') || '{}'); } catch (e) {} |
| 55 |
|
| 56 |
var o = opts; |
| 57 |
var mode = o.mode || 'wake'; |
| 58 |
var onsetDly = parseInt(o.onsetDelay) || 14; |
| 59 |
var cMin = parseInt(o.cyclesMin) || 4; |
| 60 |
var cMax = parseInt(o.cyclesMax) || 6; |
| 61 |
var timeSz = o.timeSize || 36; |
| 62 |
var radius = (o.cardRadius || 16) + 'px'; |
| 63 |
var accentC = o.accentColor || '#6c3fb5'; |
| 64 |
var bestC = o.bestColor || '#6c3fb5'; |
| 65 |
var goodC = o.goodColor || '#3b82f6'; |
| 66 |
var okC = o.okColor || '#f59e0b'; |
| 67 |
var tagBg = o.cycleTagBg || '#ede9fe'; |
| 68 |
var tagC = o.cycleTagColor || '#6c3fb5'; |
| 69 |
var labelC = o.labelColor || '#374151'; |
| 70 |
var resultBg = o.resultBg || '#f5f3ff'; |
| 71 |
var resultBdr = o.resultBorder || '#ede9fe'; |
| 72 |
var tipBg = o.tipBg || '#fef9c3'; |
| 73 |
var tipC = o.tipColor || '#713f12'; |
| 74 |
|
| 75 |
var now = new Date(); |
| 76 |
var selH = now.getHours(); |
| 77 |
var selM = Math.floor(now.getMinutes() / 5) * 5; |
| 78 |
|
| 79 |
app.innerHTML = ''; |
| 80 |
app.style.fontFamily = 'inherit'; |
| 81 |
app.style.boxSizing = 'border-box'; |
| 82 |
|
| 83 |
typoCssVarsForEl(app, o.titleTypo, '--bkslp-tt-'); |
| 84 |
typoCssVarsForEl(app, o.subtitleTypo, '--bkslp-st-'); |
| 85 |
|
| 86 |
// Section |
| 87 |
var section = document.createElement('div'); |
| 88 |
section.className = 'bkbg-sleep-section'; |
| 89 |
section.style.paddingTop = (o.paddingTop || 60) + 'px'; |
| 90 |
section.style.paddingBottom = (o.paddingBottom || 60) + 'px'; |
| 91 |
section.style.background = o.sectionBg || ''; |
| 92 |
app.appendChild(section); |
| 93 |
|
| 94 |
var card = document.createElement('div'); |
| 95 |
card.style.background = o.cardBg || '#ffffff'; |
| 96 |
card.style.borderRadius = radius; |
| 97 |
card.style.padding = '36px 32px'; |
| 98 |
card.style.maxWidth = (o.maxWidth || 580) + 'px'; |
| 99 |
card.style.margin = '0 auto'; |
| 100 |
card.style.boxShadow = '0 4px 24px rgba(0,0,0,0.09)'; |
| 101 |
card.style.boxSizing = 'border-box'; |
| 102 |
section.appendChild(card); |
| 103 |
|
| 104 |
// Title / subtitle |
| 105 |
if (o.showTitle || o.showSubtitle) { |
| 106 |
var hdr = document.createElement('div'); |
| 107 |
hdr.style.marginBottom = '20px'; |
| 108 |
if (o.showTitle) { |
| 109 |
var ttl = document.createElement('div'); |
| 110 |
ttl.className = 'bkbg-sleep-title'; |
| 111 |
ttl.textContent = o.title || 'Sleep Calculator'; |
| 112 |
ttl.style.color = o.titleColor || '#1e1b4b'; |
| 113 |
ttl.style.marginBottom = '6px'; |
| 114 |
hdr.appendChild(ttl); |
| 115 |
} |
| 116 |
if (o.showSubtitle && o.subtitle) { |
| 117 |
var sub = document.createElement('div'); |
| 118 |
sub.className = 'bkbg-sleep-subtitle'; |
| 119 |
sub.textContent = o.subtitle; |
| 120 |
sub.style.color = o.subtitleColor || '#6b7280'; |
| 121 |
hdr.appendChild(sub); |
| 122 |
} |
| 123 |
card.appendChild(hdr); |
| 124 |
} |
| 125 |
|
| 126 |
// Mode tab row |
| 127 |
var tabRow = document.createElement('div'); |
| 128 |
tabRow.style.cssText = 'display:flex;gap:10px;margin-bottom:24px;'; |
| 129 |
var modeLabelWake = '🌙 I want to wake up at\u2026'; |
| 130 |
var modeLabelSleep = '⏰ I need to wake up at\u2026'; |
| 131 |
var modes = [['wake', modeLabelWake], ['sleep', modeLabelSleep]]; |
| 132 |
var modeBtns = {}; |
| 133 |
|
| 134 |
modes.forEach(function (pair) { |
| 135 |
var m = pair[0]; var lbl = pair[1]; |
| 136 |
var btn = document.createElement('button'); |
| 137 |
btn.type = 'button'; |
| 138 |
btn.textContent = lbl; |
| 139 |
btn.className = 'bkbg-sleep-mode-btn'; |
| 140 |
btn.style.cssText = 'flex:1;padding:10px;border-radius:8px;font-family:inherit;font-size:14px;font-weight:700;cursor:pointer;transition:all .15s;'; |
| 141 |
btn.addEventListener('click', function () { |
| 142 |
mode = m; |
| 143 |
refreshTabs(); |
| 144 |
refreshLabel(); |
| 145 |
renderResults(); |
| 146 |
}); |
| 147 |
modeBtns[m] = btn; |
| 148 |
tabRow.appendChild(btn); |
| 149 |
}); |
| 150 |
card.appendChild(tabRow); |
| 151 |
|
| 152 |
function refreshTabs() { |
| 153 |
['wake', 'sleep'].forEach(function (m2) { |
| 154 |
var btn = modeBtns[m2]; |
| 155 |
var a2 = m2 === mode; |
| 156 |
btn.style.background = a2 ? accentC : 'transparent'; |
| 157 |
btn.style.color = a2 ? '#fff' : accentC; |
| 158 |
btn.style.border = '2px solid ' + (a2 ? accentC : '#e5e7eb'); |
| 159 |
}); |
| 160 |
} |
| 161 |
refreshTabs(); |
| 162 |
|
| 163 |
// Label above time row |
| 164 |
var timeLabel = document.createElement('div'); |
| 165 |
timeLabel.style.cssText = 'font-size:13px;font-weight:600;margin-bottom:8px;color:' + labelC + ';'; |
| 166 |
card.appendChild(timeLabel); |
| 167 |
|
| 168 |
function refreshLabel() { |
| 169 |
timeLabel.textContent = mode === 'wake' ? 'I plan to go to bed at:' : 'I need to wake up at:'; |
| 170 |
} |
| 171 |
refreshLabel(); |
| 172 |
|
| 173 |
// Time selector |
| 174 |
var timeRow = document.createElement('div'); |
| 175 |
timeRow.className = 'bkbg-sleep-time-row'; |
| 176 |
timeRow.style.cssText = 'display:flex;align-items:center;gap:10px;margin-bottom:24px;'; |
| 177 |
|
| 178 |
function makeSelect(opts2, initVal, onChange) { |
| 179 |
var sel = document.createElement('select'); |
| 180 |
sel.style.cssText = 'padding:10px 14px;font-size:20px;font-weight:700;border:2px solid #e5e7eb;border-radius:' + (o.inputRadius || 8) + 'px;background:#fff;cursor:pointer;'; |
| 181 |
opts2.forEach(function (opt) { |
| 182 |
var op = document.createElement('option'); |
| 183 |
op.value = opt.value; |
| 184 |
op.textContent = opt.label; |
| 185 |
if (String(opt.value) === String(initVal)) op.selected = true; |
| 186 |
sel.appendChild(op); |
| 187 |
}); |
| 188 |
sel.addEventListener('change', function () { onChange(sel.value); renderResults(); }); |
| 189 |
return sel; |
| 190 |
} |
| 191 |
|
| 192 |
var hourOpts = []; |
| 193 |
for (var h2 = 0; h2 < 24; h2++) { |
| 194 |
var ampm2 = h2 < 12 ? 'AM' : 'PM'; |
| 195 |
var hh2 = h2 % 12 || 12; |
| 196 |
hourOpts.push({ label: hh2 + ' ' + ampm2, value: String(h2) }); |
| 197 |
} |
| 198 |
var minOpts = []; |
| 199 |
for (var mm = 0; mm < 60; mm += 5) { minOpts.push({ label: pad2(mm), value: String(mm) }); } |
| 200 |
|
| 201 |
var hSel = makeSelect(hourOpts, selH, function (v) { selH = parseInt(v, 10); }); |
| 202 |
var sep = document.createElement('span'); |
| 203 |
sep.textContent = ':'; |
| 204 |
sep.style.cssText = 'font-size:22px;font-weight:700;color:' + labelC + ';'; |
| 205 |
var mSel = makeSelect(minOpts, selM, function (v) { selM = parseInt(v, 10); }); |
| 206 |
|
| 207 |
timeRow.appendChild(hSel); |
| 208 |
timeRow.appendChild(sep); |
| 209 |
timeRow.appendChild(mSel); |
| 210 |
card.appendChild(timeRow); |
| 211 |
|
| 212 |
// Results label |
| 213 |
var resLabel = document.createElement('div'); |
| 214 |
resLabel.style.cssText = 'font-size:13px;font-weight:600;margin-bottom:12px;color:' + labelC + ';'; |
| 215 |
card.appendChild(resLabel); |
| 216 |
|
| 217 |
// Results container |
| 218 |
var resContainer = document.createElement('div'); |
| 219 |
resContainer.className = 'bkbg-sleep-results'; |
| 220 |
resContainer.style.marginBottom = o.showTips ? '20px' : '0'; |
| 221 |
card.appendChild(resContainer); |
| 222 |
|
| 223 |
// Tips |
| 224 |
if (o.showTips) { |
| 225 |
var tipBox = document.createElement('div'); |
| 226 |
tipBox.className = 'bkbg-sleep-tip'; |
| 227 |
tipBox.style.cssText = 'background:' + tipBg + ';color:' + tipC + ';border-radius:' + radius + ';padding:14px 18px;font-size:13px;line-height:1.6;'; |
| 228 |
tipBox.innerHTML = '<strong>💡 Tip:</strong> Sleep comes in 90-minute cycles. Waking at the end of a cycle helps you feel more refreshed. Allow ' + onsetDly + ' minutes to fall asleep.'; |
| 229 |
card.appendChild(tipBox); |
| 230 |
} |
| 231 |
|
| 232 |
function qualColor(c) { |
| 233 |
if (c >= 6) return bestC; |
| 234 |
if (c >= 5) return goodC; |
| 235 |
return okC; |
| 236 |
} |
| 237 |
|
| 238 |
function renderResults() { |
| 239 |
var results = calcTimes(mode, selH, selM, onsetDly, cMin, cMax); |
| 240 |
resLabel.textContent = mode === 'wake' ? 'Set your alarm for one of these times:' : 'Try to fall asleep at one of these times:'; |
| 241 |
resContainer.innerHTML = ''; |
| 242 |
results.forEach(function (r, i) { |
| 243 |
var qC = qualColor(r.cycles); |
| 244 |
var totalH = Math.floor(r.totalSleep / 60); |
| 245 |
var totalM = r.totalSleep % 60; |
| 246 |
var row = document.createElement('div'); |
| 247 |
row.className = 'bkbg-sleep-result-row'; |
| 248 |
row.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:14px 18px;margin-bottom:8px;background:' + resultBg + ';border:1.5px solid ' + resultBdr + ';border-left:4px solid ' + (i === 0 ? qC : 'transparent') + ';border-radius:' + radius + ';transition:box-shadow .18s,transform .18s;box-sizing:border-box;'; |
| 249 |
|
| 250 |
var left = document.createElement('div'); |
| 251 |
var timeEl = document.createElement('div'); |
| 252 |
timeEl.textContent = fmt12(r.h, r.m); |
| 253 |
timeEl.style.cssText = 'font-size:' + timeSz + 'px;font-weight:800;color:' + qC + ';line-height:1.1;'; |
| 254 |
var durEl = document.createElement('div'); |
| 255 |
durEl.textContent = totalH + 'h' + (totalM ? ' ' + totalM + 'm' : '') + ' of sleep'; |
| 256 |
durEl.style.cssText = 'font-size:12px;color:' + labelC + ';margin-top:2px;'; |
| 257 |
left.appendChild(timeEl); |
| 258 |
left.appendChild(durEl); |
| 259 |
|
| 260 |
var right = document.createElement('div'); |
| 261 |
right.style.textAlign = 'right'; |
| 262 |
var tag = document.createElement('div'); |
| 263 |
tag.textContent = r.cycles + ' cycles'; |
| 264 |
tag.style.cssText = 'display:inline-block;font-weight:700;font-size:12px;background:' + tagBg + ';color:' + tagC + ';padding:3px 10px;border-radius:100px;margin-bottom:3px;'; |
| 265 |
right.appendChild(tag); |
| 266 |
if (i === 0) { |
| 267 |
var best = document.createElement('div'); |
| 268 |
best.textContent = '� |
| 269 |
Best'; |
| 270 |
best.style.cssText = 'font-size:11px;color:' + qC + ';font-weight:600;margin-top:2px;'; |
| 271 |
right.appendChild(best); |
| 272 |
} |
| 273 |
|
| 274 |
row.appendChild(left); |
| 275 |
row.appendChild(right); |
| 276 |
resContainer.appendChild(row); |
| 277 |
}); |
| 278 |
} |
| 279 |
|
| 280 |
renderResults(); |
| 281 |
} |
| 282 |
|
| 283 |
document.querySelectorAll('.bkbg-sleep-app').forEach(buildApp); |
| 284 |
})(); |
| 285 |
|