| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
/* ── Static indicative rates (USD base) ─────────────────────────────── */ |
| 5 |
var USD_RATES = { |
| 6 |
USD:1,EUR:0.92,GBP:0.79,JPY:149.5,CAD:1.36,AUD:1.53,CHF:0.90,CNY:7.24, |
| 7 |
INR:83.1,MXN:17.15,BRL:4.97,KRW:1325,SGD:1.34,HKD:7.82,NOK:10.58,SEK:10.42, |
| 8 |
DKK:6.88,NZD:1.63,ZAR:18.63,TRY:32.6,RUB:90.5,PLN:4.03,THB:35.1,MYR:4.72, |
| 9 |
IDR:15650,PHP:56.5,AED:3.67,SAR:3.75,EGP:30.9,NGN:1490, |
| 10 |
}; |
| 11 |
|
| 12 |
var CURRENCY_NAMES = { |
| 13 |
USD:'US Dollar',EUR:'Euro',GBP:'British Pound',JPY:'Japanese Yen',CAD:'Canadian Dollar', |
| 14 |
AUD:'Australian Dollar',CHF:'Swiss Franc',CNY:'Chinese Yuan',INR:'Indian Rupee', |
| 15 |
MXN:'Mexican Peso',BRL:'Brazilian Real',KRW:'South Korean Won',SGD:'Singapore Dollar', |
| 16 |
HKD:'Hong Kong Dollar',NOK:'Norwegian Krone',SEK:'Swedish Krona',DKK:'Danish Krone', |
| 17 |
NZD:'New Zealand Dollar',ZAR:'South African Rand',TRY:'Turkish Lira',RUB:'Russian Ruble', |
| 18 |
PLN:'Polish Zloty',THB:'Thai Baht',MYR:'Malaysian Ringgit',IDR:'Indonesian Rupiah', |
| 19 |
PHP:'Philippine Peso',AED:'UAE Dirham',SAR:'Saudi Riyal',EGP:'Egyptian Pound',NGN:'Nigerian Naira', |
| 20 |
}; |
| 21 |
|
| 22 |
function convert(amount, from, to) { |
| 23 |
if (!USD_RATES[from] || !USD_RATES[to]) return 0; |
| 24 |
return (amount / USD_RATES[from]) * USD_RATES[to]; |
| 25 |
} |
| 26 |
|
| 27 |
function fmtNum(val) { |
| 28 |
if (isNaN(val) || val === null) return '—'; |
| 29 |
if (val >= 1000) return val.toLocaleString('en-US', { maximumFractionDigits: 2 }); |
| 30 |
return val.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 4 }); |
| 31 |
} |
| 32 |
|
| 33 |
function cname(code) { return CURRENCY_NAMES[code] || code; } |
| 34 |
|
| 35 |
/* ── Typography CSS-var helper ─────────────────────────────────────── */ |
| 36 |
function typoCssVarsForEl(typo, prefix, el) { |
| 37 |
if (!typo || typeof typo !== 'object') return; |
| 38 |
var map = { |
| 39 |
family:'font-family', weight:'font-weight', style:'font-style', |
| 40 |
transform:'text-transform', decoration:'text-decoration', |
| 41 |
sizeDesktop:'font-size-d', sizeTablet:'font-size-t', sizeMobile:'font-size-m', sizeUnit:'font-size-unit', |
| 42 |
lineHeightDesktop:'line-height-d', lineHeightTablet:'line-height-t', lineHeightMobile:'line-height-m', lineHeightUnit:'line-height-unit', |
| 43 |
letterSpacingDesktop:'letter-spacing-d', letterSpacingTablet:'letter-spacing-t', letterSpacingMobile:'letter-spacing-m', letterSpacingUnit:'letter-spacing-unit', |
| 44 |
wordSpacingDesktop:'word-spacing-d', wordSpacingTablet:'word-spacing-t', wordSpacingMobile:'word-spacing-m', wordSpacingUnit:'word-spacing-unit' |
| 45 |
}; |
| 46 |
Object.keys(map).forEach(function(k) { |
| 47 |
if (typo[k] !== undefined && typo[k] !== '') { |
| 48 |
var v = typo[k]; |
| 49 |
var css = map[k]; |
| 50 |
if (css.indexOf('size-d') !== -1 || css.indexOf('size-t') !== -1 || css.indexOf('size-m') !== -1 || |
| 51 |
css.indexOf('height-d') !== -1 || css.indexOf('height-t') !== -1 || css.indexOf('height-m') !== -1 || |
| 52 |
css.indexOf('spacing-d') !== -1 || css.indexOf('spacing-t') !== -1 || css.indexOf('spacing-m') !== -1) { |
| 53 |
var unitKey = css.replace(/-[dtm]$/, '-unit'); |
| 54 |
var unit = ''; |
| 55 |
Object.keys(map).forEach(function(k2) { if (map[k2] === unitKey && typo[k2]) unit = typo[k2]; }); |
| 56 |
if (!unit) unit = css.indexOf('size') !== -1 ? 'px' : ''; |
| 57 |
v = v + unit; |
| 58 |
} |
| 59 |
el.style.setProperty(prefix + css, v); |
| 60 |
} |
| 61 |
}); |
| 62 |
} |
| 63 |
|
| 64 |
/* ── Build select options ─────────────────────────────────────────── */ |
| 65 |
function buildSelectOptions(sel, selected) { |
| 66 |
sel.innerHTML = ''; |
| 67 |
Object.keys(USD_RATES).forEach(function (code) { |
| 68 |
var opt = document.createElement('option'); |
| 69 |
opt.value = code; |
| 70 |
opt.textContent = code + ' — ' + cname(code); |
| 71 |
if (code === selected) opt.selected = true; |
| 72 |
sel.appendChild(opt); |
| 73 |
}); |
| 74 |
} |
| 75 |
|
| 76 |
/* ── Main builder ─────────────────────────────────────────────────── */ |
| 77 |
function buildApp(app) { |
| 78 |
var opts = {}; |
| 79 |
try { opts = JSON.parse(app.getAttribute('data-opts') || '{}'); } catch (e) {} |
| 80 |
|
| 81 |
var accent = opts.accentColor || '#6c3fb5'; |
| 82 |
var cardBg = opts.cardBg || '#ffffff'; |
| 83 |
var resultBg = opts.resultBg || '#f5f3ff'; |
| 84 |
var resultBdr = opts.resultBorder || '#ede9fe'; |
| 85 |
var resultClr = opts.resultColor || accent; |
| 86 |
var labelColor = opts.labelColor || '#374151'; |
| 87 |
var titleColor = opts.titleColor || '#1e1b4b'; |
| 88 |
var subColor = opts.subtitleColor || '#6b7280'; |
| 89 |
var pairBg = opts.quickPairBg || '#f3f4f6'; |
| 90 |
var pairColor = opts.quickPairColor|| '#374151'; |
| 91 |
var cRadius = (opts.cardRadius !== undefined ? opts.cardRadius : 16) + 'px'; |
| 92 |
var maxW = (opts.maxWidth || 580) + 'px'; |
| 93 |
var ptop = (opts.paddingTop !== undefined ? opts.paddingTop : 60) + 'px'; |
| 94 |
var pbot = (opts.paddingBottom!== undefined ? opts.paddingBottom : 60) + 'px'; |
| 95 |
var titleSize = (opts.titleSize || 26) + 'px'; |
| 96 |
var resultSize = (opts.resultSize || 40) + 'px'; |
| 97 |
var quickPairs = opts.quickPairs || []; |
| 98 |
|
| 99 |
var fromCur = opts.defaultFrom || 'USD'; |
| 100 |
var toCur = opts.defaultTo || 'EUR'; |
| 101 |
var amount = opts.defaultAmount || 100; |
| 102 |
|
| 103 |
/* Card */ |
| 104 |
var card = document.createElement('div'); |
| 105 |
card.className = 'bkbg-cc-card'; |
| 106 |
card.style.cssText = [ |
| 107 |
'background:' + cardBg, |
| 108 |
'border-radius:' + cRadius, |
| 109 |
'padding-top:'+ ptop, |
| 110 |
'padding-bottom:' + pbot, |
| 111 |
'max-width:' + maxW, |
| 112 |
].join(';'); |
| 113 |
app.innerHTML = ''; |
| 114 |
app.appendChild(card); |
| 115 |
|
| 116 |
/* Apply typography CSS vars on card */ |
| 117 |
card.style.setProperty('--bkbg-cc-ttl-fs', titleSize); |
| 118 |
card.style.setProperty('--bkbg-cc-res-fs', resultSize); |
| 119 |
typoCssVarsForEl(opts.typoTitle, '--bkbg-cc-ttl-', card); |
| 120 |
typoCssVarsForEl(opts.typoResult, '--bkbg-cc-res-', card); |
| 121 |
|
| 122 |
/* Title */ |
| 123 |
if (opts.showTitle && opts.title) { |
| 124 |
var h2 = document.createElement('h2'); |
| 125 |
h2.className = 'bkbg-cc-title'; |
| 126 |
h2.textContent = opts.title; |
| 127 |
h2.style.color = titleColor; |
| 128 |
card.appendChild(h2); |
| 129 |
} |
| 130 |
|
| 131 |
/* Subtitle */ |
| 132 |
if (opts.showSubtitle && opts.subtitle) { |
| 133 |
var sub = document.createElement('p'); |
| 134 |
sub.className = 'bkbg-cc-subtitle'; |
| 135 |
sub.textContent = opts.subtitle; |
| 136 |
sub.style.color = subColor; |
| 137 |
card.appendChild(sub); |
| 138 |
} |
| 139 |
|
| 140 |
/* Quick pairs */ |
| 141 |
var pairsDiv; |
| 142 |
if (opts.showQuickPairs && quickPairs.length) { |
| 143 |
pairsDiv = document.createElement('div'); |
| 144 |
pairsDiv.className = 'bkbg-cc-pairs'; |
| 145 |
card.appendChild(pairsDiv); |
| 146 |
} |
| 147 |
|
| 148 |
/* Convert row: amount + swap + to */ |
| 149 |
var row = document.createElement('div'); |
| 150 |
row.className = 'bkbg-cc-convert-row'; |
| 151 |
card.appendChild(row); |
| 152 |
|
| 153 |
/* Amount */ |
| 154 |
var amtGroup = document.createElement('div'); |
| 155 |
amtGroup.className = 'bkbg-cc-field-group'; |
| 156 |
var amtLabel = document.createElement('label'); |
| 157 |
amtLabel.textContent = 'Amount'; |
| 158 |
amtLabel.style.color = labelColor; |
| 159 |
var amtInput = document.createElement('input'); |
| 160 |
amtInput.type = 'number'; |
| 161 |
amtInput.className = 'bkbg-cc-amount-input'; |
| 162 |
amtInput.value = amount; |
| 163 |
amtInput.min = '0'; |
| 164 |
amtInput.step = 'any'; |
| 165 |
amtGroup.appendChild(amtLabel); |
| 166 |
amtGroup.appendChild(amtInput); |
| 167 |
row.appendChild(amtGroup); |
| 168 |
|
| 169 |
/* Swap */ |
| 170 |
var swapBtn; |
| 171 |
if (opts.showSwapBtn) { |
| 172 |
swapBtn = document.createElement('button'); |
| 173 |
swapBtn.className = 'bkbg-cc-swap'; |
| 174 |
swapBtn.textContent = '⇄'; |
| 175 |
swapBtn.style.borderColor = accent; |
| 176 |
swapBtn.style.color = accent; |
| 177 |
row.appendChild(swapBtn); |
| 178 |
} else { |
| 179 |
var arrow = document.createElement('div'); |
| 180 |
arrow.textContent = '→'; |
| 181 |
arrow.style.cssText = 'align-self:flex-end;padding:10px 4px;font-size:20px;color:#9ca3af'; |
| 182 |
row.appendChild(arrow); |
| 183 |
} |
| 184 |
|
| 185 |
/* To select */ |
| 186 |
var toGroup = document.createElement('div'); |
| 187 |
toGroup.className = 'bkbg-cc-field-group'; |
| 188 |
var toLabel = document.createElement('label'); |
| 189 |
toLabel.textContent = 'To'; |
| 190 |
toLabel.style.color = labelColor; |
| 191 |
var toSel = document.createElement('select'); |
| 192 |
toSel.className = 'bkbg-cc-select'; |
| 193 |
buildSelectOptions(toSel, toCur); |
| 194 |
toGroup.appendChild(toLabel); |
| 195 |
toGroup.appendChild(toSel); |
| 196 |
row.appendChild(toGroup); |
| 197 |
|
| 198 |
/* From — full width row */ |
| 199 |
var fromRow = document.createElement('div'); |
| 200 |
fromRow.className = 'bkbg-cc-from-row'; |
| 201 |
var fromLabel = document.createElement('label'); |
| 202 |
fromLabel.textContent = 'From Currency'; |
| 203 |
fromLabel.style.color = labelColor; |
| 204 |
var fromSel = document.createElement('select'); |
| 205 |
fromSel.className = 'bkbg-cc-select'; |
| 206 |
buildSelectOptions(fromSel, fromCur); |
| 207 |
fromRow.appendChild(fromLabel); |
| 208 |
fromRow.appendChild(fromSel); |
| 209 |
card.appendChild(fromRow); |
| 210 |
|
| 211 |
/* Result box */ |
| 212 |
var resultBox = document.createElement('div'); |
| 213 |
resultBox.className = 'bkbg-cc-result'; |
| 214 |
resultBox.style.cssText = 'background:' + resultBg + ';border:1.5px solid ' + resultBdr + ';border-radius:' + cRadius; |
| 215 |
var resLabel = document.createElement('div'); |
| 216 |
resLabel.className = 'bkbg-cc-result-label'; |
| 217 |
var resNum = document.createElement('div'); |
| 218 |
resNum.className = 'bkbg-cc-result-num'; |
| 219 |
resNum.style.color = resultClr; |
| 220 |
var resRate = document.createElement('div'); |
| 221 |
resRate.className = 'bkbg-cc-result-rate'; |
| 222 |
resultBox.appendChild(resLabel); |
| 223 |
resultBox.appendChild(resNum); |
| 224 |
resultBox.appendChild(resRate); |
| 225 |
card.appendChild(resultBox); |
| 226 |
|
| 227 |
/* Disclaimer */ |
| 228 |
if (opts.showDisclaimer && opts.disclaimer) { |
| 229 |
var dis = document.createElement('p'); |
| 230 |
dis.className = 'bkbg-cc-disclaimer'; |
| 231 |
dis.textContent = opts.disclaimer; |
| 232 |
card.appendChild(dis); |
| 233 |
} |
| 234 |
|
| 235 |
/* ── Update result ──────────────────────────────────────────────── */ |
| 236 |
function updateResult() { |
| 237 |
var amt = parseFloat(amtInput.value) || 0; |
| 238 |
var fr = fromSel.value; |
| 239 |
var to = toSel.value; |
| 240 |
var res = convert(amt, fr, to); |
| 241 |
var rate = convert(1, fr, to); |
| 242 |
resLabel.textContent = amt + ' ' + fr + ' equals'; |
| 243 |
resNum.textContent = fmtNum(res) + ' ' + to; |
| 244 |
resRate.textContent = '1 ' + fr + ' = ' + fmtNum(rate) + ' ' + to; |
| 245 |
/* update quick pair active state */ |
| 246 |
if (pairsDiv) { |
| 247 |
pairsDiv.querySelectorAll('.bkbg-cc-pair-btn').forEach(function (btn) { |
| 248 |
var active = btn.dataset.from === fr && btn.dataset.to === to; |
| 249 |
btn.classList.toggle('active', active); |
| 250 |
btn.style.background = active ? accent : pairBg; |
| 251 |
btn.style.color = active ? '#fff' : pairColor; |
| 252 |
}); |
| 253 |
} |
| 254 |
} |
| 255 |
|
| 256 |
/* ── Quick pairs buttons ────────────────────────────────────────── */ |
| 257 |
if (pairsDiv && quickPairs.length) { |
| 258 |
quickPairs.forEach(function (pair) { |
| 259 |
var btn = document.createElement('button'); |
| 260 |
btn.className = 'bkbg-cc-pair-btn'; |
| 261 |
btn.dataset.from = pair[0]; |
| 262 |
btn.dataset.to = pair[1]; |
| 263 |
btn.textContent = pair[0] + '/' + pair[1]; |
| 264 |
btn.style.background = (pair[0] === fromCur && pair[1] === toCur) ? accent : pairBg; |
| 265 |
btn.style.color = (pair[0] === fromCur && pair[1] === toCur) ? '#fff' : pairColor; |
| 266 |
btn.addEventListener('click', function () { |
| 267 |
fromSel.value = pair[0]; |
| 268 |
toSel.value = pair[1]; |
| 269 |
updateResult(); |
| 270 |
}); |
| 271 |
pairsDiv.appendChild(btn); |
| 272 |
}); |
| 273 |
} |
| 274 |
|
| 275 |
/* ── Swap ─────────────────────────────────────────────────────── */ |
| 276 |
if (swapBtn) { |
| 277 |
swapBtn.addEventListener('click', function () { |
| 278 |
var tmp = fromSel.value; |
| 279 |
fromSel.value = toSel.value; |
| 280 |
toSel.value = tmp; |
| 281 |
updateResult(); |
| 282 |
}); |
| 283 |
} |
| 284 |
|
| 285 |
/* ── Events ───────────────────────────────────────────────────── */ |
| 286 |
amtInput.addEventListener('input', updateResult); |
| 287 |
fromSel.addEventListener('change', updateResult); |
| 288 |
toSel.addEventListener('change', updateResult); |
| 289 |
|
| 290 |
/* Initial render */ |
| 291 |
updateResult(); |
| 292 |
} |
| 293 |
|
| 294 |
/* ── Init all instances ───────────────────────────────────────────── */ |
| 295 |
document.querySelectorAll('.bkbg-cc-app').forEach(buildApp); |
| 296 |
})(); |
| 297 |
|