| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
var DEFAULT_CATEGORIES = [ |
| 5 |
{ name:'Housing', amount:1200, icon:'🏠' }, |
| 6 |
{ name:'Food', amount:400, icon:'🍔' }, |
| 7 |
{ name:'Transport', amount:250, icon:'🚗' }, |
| 8 |
{ name:'Utilities', amount:150, icon:'💡' }, |
| 9 |
{ name:'Healthcare', amount:100, icon:'🏥' }, |
| 10 |
{ name:'Entertainment', amount:200, icon:'🎬' } |
| 11 |
]; |
| 12 |
|
| 13 |
var BUDGETING_TIPS = [ |
| 14 |
'The 50/30/20 rule: spend 50% on needs, 30% on wants, and save 20%.', |
| 15 |
'Build a 3–6 month emergency fund before investing.', |
| 16 |
'Track every expense — even small ones add up over a month.', |
| 17 |
'Pay yourself first: move savings out on payday before spending.', |
| 18 |
'Review subscriptions quarterly — cancel what you no longer use.' |
| 19 |
]; |
| 20 |
|
| 21 |
function typoCssVarsForEl(typo, prefix, el) { |
| 22 |
if (!typo) return; |
| 23 |
if (typo.family) el.style.setProperty(prefix + 'font-family', "'" + typo.family + "', sans-serif"); |
| 24 |
if (typo.weight) el.style.setProperty(prefix + 'font-weight', typo.weight); |
| 25 |
if (typo.transform) el.style.setProperty(prefix + 'text-transform', typo.transform); |
| 26 |
if (typo.style) el.style.setProperty(prefix + 'font-style', typo.style); |
| 27 |
if (typo.decoration) el.style.setProperty(prefix + 'text-decoration', typo.decoration); |
| 28 |
var su = typo.sizeUnit || 'px'; |
| 29 |
if (typo.sizeDesktop !== '' && typo.sizeDesktop != null) el.style.setProperty(prefix + 'font-size-d', typo.sizeDesktop + su); |
| 30 |
if (typo.sizeTablet !== '' && typo.sizeTablet != null) el.style.setProperty(prefix + 'font-size-t', typo.sizeTablet + su); |
| 31 |
if (typo.sizeMobile !== '' && typo.sizeMobile != null) el.style.setProperty(prefix + 'font-size-m', typo.sizeMobile + su); |
| 32 |
var lhu = typo.lineHeightUnit || ''; |
| 33 |
if (typo.lineHeightDesktop !== '' && typo.lineHeightDesktop != null) el.style.setProperty(prefix + 'line-height-d', typo.lineHeightDesktop + lhu); |
| 34 |
if (typo.lineHeightTablet !== '' && typo.lineHeightTablet != null) el.style.setProperty(prefix + 'line-height-t', typo.lineHeightTablet + lhu); |
| 35 |
if (typo.lineHeightMobile !== '' && typo.lineHeightMobile != null) el.style.setProperty(prefix + 'line-height-m', typo.lineHeightMobile + lhu); |
| 36 |
var lsu = typo.letterSpacingUnit || 'px'; |
| 37 |
if (typo.letterSpacingDesktop !== '' && typo.letterSpacingDesktop != null) el.style.setProperty(prefix + 'letter-spacing-d', typo.letterSpacingDesktop + lsu); |
| 38 |
if (typo.letterSpacingTablet !== '' && typo.letterSpacingTablet != null) el.style.setProperty(prefix + 'letter-spacing-t', typo.letterSpacingTablet + lsu); |
| 39 |
if (typo.letterSpacingMobile !== '' && typo.letterSpacingMobile != null) el.style.setProperty(prefix + 'letter-spacing-m', typo.letterSpacingMobile + lsu); |
| 40 |
var wsu = typo.wordSpacingUnit || 'px'; |
| 41 |
if (typo.wordSpacingDesktop !== '' && typo.wordSpacingDesktop != null) el.style.setProperty(prefix + 'word-spacing-d', typo.wordSpacingDesktop + wsu); |
| 42 |
if (typo.wordSpacingTablet !== '' && typo.wordSpacingTablet != null) el.style.setProperty(prefix + 'word-spacing-t', typo.wordSpacingTablet + wsu); |
| 43 |
if (typo.wordSpacingMobile !== '' && typo.wordSpacingMobile != null) el.style.setProperty(prefix + 'word-spacing-m', typo.wordSpacingMobile + wsu); |
| 44 |
} |
| 45 |
|
| 46 |
document.querySelectorAll('.bkbg-bp-app').forEach(function(root) { |
| 47 |
var opts; |
| 48 |
try { opts = JSON.parse(root.getAttribute('data-opts') || '{}'); } |
| 49 |
catch(e) { return; } |
| 50 |
|
| 51 |
typoCssVarsForEl(opts.typoTitle, '--bkbg-bp-title-', root); |
| 52 |
typoCssVarsForEl(opts.typoSubtitle, '--bkbg-bp-sub-', root); |
| 53 |
|
| 54 |
var o = { |
| 55 |
title: opts.title || 'Monthly Budget Planner', |
| 56 |
subtitle: opts.subtitle || '', |
| 57 |
currency: opts.currency || '$', |
| 58 |
showSavingsGoal: opts.showSavingsGoal !== false, |
| 59 |
showPieBreakdown:opts.showPieBreakdown !== false, |
| 60 |
showTips: opts.showTips !== false, |
| 61 |
defaultIncome: parseFloat(opts.defaultIncome) || 5000, |
| 62 |
defaultSavings: parseFloat(opts.defaultSavings) || 500, |
| 63 |
accentColor: opts.accentColor || '#22c55e', |
| 64 |
expenseColor: opts.expenseColor || '#f43f5e', |
| 65 |
savingsColor: opts.savingsColor || '#8b5cf6', |
| 66 |
surplusColor: opts.surplusColor || '#0ea5e9', |
| 67 |
sectionBg: opts.sectionBg || '#f0fdf4', |
| 68 |
cardBg: opts.cardBg || '#ffffff', |
| 69 |
inputBg: opts.inputBg || '#f9fafb', |
| 70 |
titleColor: opts.titleColor || '#111827', |
| 71 |
subtitleColor: opts.subtitleColor || '#6b7280', |
| 72 |
labelColor: opts.labelColor || '#374151', |
| 73 |
titleFontSize: parseInt(opts.titleFontSize) || 26, |
| 74 |
contentMaxWidth: parseInt(opts.contentMaxWidth) || 740 |
| 75 |
}; |
| 76 |
|
| 77 |
var income = o.defaultIncome; |
| 78 |
var savingsGoal= o.defaultSavings; |
| 79 |
var categories = DEFAULT_CATEGORIES.map(function(c){ return { name:c.name, amount:c.amount, icon:c.icon }; }); |
| 80 |
|
| 81 |
function cur(n) { return o.currency + Number(n).toLocaleString(undefined, {minimumFractionDigits:0, maximumFractionDigits:2}); } |
| 82 |
|
| 83 |
// ── Build shell ──────────────────────────────────────────────── |
| 84 |
root.innerHTML = |
| 85 |
'<div class="bkbg-bp-wrap" style="background:' + o.sectionBg + ';max-width:' + o.contentMaxWidth + 'px;--bp-accent:' + o.accentColor + '">' + |
| 86 |
(o.title ? '<h3 class="bkbg-bp-title" style="color:' + o.titleColor + '">' + o.title + '</h3>' : '') + |
| 87 |
(o.subtitle ? '<p class="bkbg-bp-subtitle" style="color:' + o.subtitleColor + '">' + o.subtitle + '</p>' : '') + |
| 88 |
|
| 89 |
// Income |
| 90 |
'<div class="bkbg-bp-income-card" style="background:' + o.cardBg + '">' + |
| 91 |
'<span class="bkbg-bp-income-icon">💰</span>' + |
| 92 |
'<div class="bkbg-bp-income-info">' + |
| 93 |
'<div class="bkbg-bp-income-caption">Monthly Income</div>' + |
| 94 |
'<div class="bkbg-bp-income-value" id="bp-income-disp" style="color:' + o.accentColor + '">' + cur(income) + '</div>' + |
| 95 |
'</div>' + |
| 96 |
'<input id="bp-income-in" type="number" class="bkbg-bp-income-input" value="' + income + '" min="0" step="100" style="background:' + o.inputBg + ';color:' + o.labelColor + '">' + |
| 97 |
'</div>' + |
| 98 |
|
| 99 |
// Categories section |
| 100 |
'<div class="bkbg-bp-section-title" style="color:' + o.labelColor + '">Expense Categories</div>' + |
| 101 |
'<div class="bkbg-bp-categories" id="bp-cats"></div>' + |
| 102 |
|
| 103 |
// Add category row |
| 104 |
'<div class="bkbg-bp-add-row">' + |
| 105 |
'<input id="bp-new-name" type="text" class="bkbg-bp-add-input" placeholder="Category name (e.g. Gym)" style="background:' + o.inputBg + ';color:' + o.labelColor + '">' + |
| 106 |
'<input id="bp-new-amount" type="number" class="bkbg-bp-add-input" placeholder="Amount" min="0" step="10" style="background:' + o.inputBg + ';color:' + o.labelColor + ';max-width:100px">' + |
| 107 |
'<button id="bp-add-cat" class="bkbg-bp-add-btn">+ Add</button>' + |
| 108 |
'</div>' + |
| 109 |
|
| 110 |
// Savings goal |
| 111 |
(o.showSavingsGoal ? |
| 112 |
'<div class="bkbg-bp-savings-card" style="background:' + o.cardBg + '">' + |
| 113 |
'<span style="font-size:22px">🎯</span>' + |
| 114 |
'<div style="flex:1"><div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:#6b7280;margin-bottom:3px">Savings Goal</div>' + |
| 115 |
'<div id="bp-sav-disp" style="font-size:20px;font-weight:700;color:' + o.savingsColor + '">' + cur(savingsGoal) + '</div></div>' + |
| 116 |
'<input id="bp-sav-in" type="number" class="bkbg-bp-income-input" value="' + savingsGoal + '" min="0" step="50" style="background:' + o.inputBg + ';color:' + o.labelColor + '">' + |
| 117 |
'</div>' |
| 118 |
: '') + |
| 119 |
|
| 120 |
// Summary |
| 121 |
'<div class="bkbg-bp-summary" id="bp-summary"></div>' + |
| 122 |
|
| 123 |
// Allocation bar |
| 124 |
(o.showPieBreakdown ? '<div class="bkbg-bp-alloc" id="bp-alloc"></div>' : '') + |
| 125 |
|
| 126 |
// Tips |
| 127 |
(o.showTips ? |
| 128 |
'<div class="bkbg-bp-tips">' + |
| 129 |
'<div class="bkbg-bp-tips-title">💡 Budgeting Tips</div>' + |
| 130 |
'<ul class="bkbg-bp-tips-list">' + |
| 131 |
BUDGETING_TIPS.map(function(t){ return '<li>' + t + '</li>'; }).join('') + |
| 132 |
'</ul></div>' |
| 133 |
: '') + |
| 134 |
'</div>'; |
| 135 |
|
| 136 |
var catsEl = root.querySelector('#bp-cats'); |
| 137 |
var summaryEl = root.querySelector('#bp-summary'); |
| 138 |
var allocEl = root.querySelector('#bp-alloc'); |
| 139 |
var incomeIn = root.querySelector('#bp-income-in'); |
| 140 |
var incomeDisp= root.querySelector('#bp-income-disp'); |
| 141 |
var savIn = root.querySelector('#bp-sav-in'); |
| 142 |
var savDisp = root.querySelector('#bp-sav-disp'); |
| 143 |
var newName = root.querySelector('#bp-new-name'); |
| 144 |
var newAmt = root.querySelector('#bp-new-amount'); |
| 145 |
var addBtn = root.querySelector('#bp-add-cat'); |
| 146 |
|
| 147 |
// ── Render categories ────────────────────────────────────────── |
| 148 |
function renderCategories() { |
| 149 |
var totalExp = categories.reduce(function(s,c){ return s + (parseFloat(c.amount)||0); }, 0); |
| 150 |
catsEl.innerHTML = categories.map(function(cat, idx) { |
| 151 |
var pct = income > 0 ? Math.min(100, (cat.amount/income)*100) : 0; |
| 152 |
return '<div class="bkbg-bp-cat-row" style="background:' + o.cardBg + '">' + |
| 153 |
'<span class="bkbg-bp-cat-icon">' + (cat.icon||'📋') + '</span>' + |
| 154 |
'<span class="bkbg-bp-cat-name" style="color:' + o.labelColor + '">' + escHtml(cat.name) + '</span>' + |
| 155 |
'<div class="bkbg-bp-cat-bar-wrap">' + |
| 156 |
'<div class="bkbg-bp-cat-bar-fill" style="width:' + pct.toFixed(1) + '%;background:' + o.expenseColor + '"></div>' + |
| 157 |
'</div>' + |
| 158 |
'<input class="bkbg-bp-cat-input" type="number" data-idx="' + idx + '" value="' + cat.amount + '" min="0" step="10" style="background:' + o.inputBg + ';color:' + o.labelColor + '">' + |
| 159 |
'<button class="bkbg-bp-del-cat" data-idx="' + idx + '" style="background:none;border:none;font-size:16px;cursor:pointer;color:#9ca3af;padding:0 2px" title="Remove">✕</button>' + |
| 160 |
'</div>'; |
| 161 |
}).join(''); |
| 162 |
|
| 163 |
// bind inputs |
| 164 |
catsEl.querySelectorAll('.bkbg-bp-cat-input').forEach(function(inp) { |
| 165 |
inp.addEventListener('input', function() { |
| 166 |
var idx = parseInt(inp.getAttribute('data-idx')); |
| 167 |
categories[idx].amount = parseFloat(inp.value) || 0; |
| 168 |
renderSummary(); |
| 169 |
}); |
| 170 |
}); |
| 171 |
catsEl.querySelectorAll('.bkbg-bp-del-cat').forEach(function(btn) { |
| 172 |
btn.addEventListener('click', function() { |
| 173 |
var idx = parseInt(btn.getAttribute('data-idx')); |
| 174 |
categories.splice(idx, 1); |
| 175 |
renderCategories(); |
| 176 |
renderSummary(); |
| 177 |
}); |
| 178 |
}); |
| 179 |
} |
| 180 |
|
| 181 |
// ── Render summary ───────────────────────────────────────────── |
| 182 |
function renderSummary() { |
| 183 |
var totalExp = categories.reduce(function(s,c){ return s + (parseFloat(c.amount)||0); }, 0); |
| 184 |
var sav = o.showSavingsGoal ? savingsGoal : 0; |
| 185 |
var surplus = income - totalExp - sav; |
| 186 |
var surColor = surplus >= 0 ? o.surplusColor : o.expenseColor; |
| 187 |
var surLabel = surplus >= 0 ? 'Surplus' : 'Deficit'; |
| 188 |
|
| 189 |
var cards = [ |
| 190 |
{ label:'Total Income', value: cur(income), color: o.accentColor }, |
| 191 |
{ label:'Total Expenses', value: cur(totalExp), color: o.expenseColor }, |
| 192 |
{ label:'Savings Goal', value: cur(sav), color: o.savingsColor, hide: !o.showSavingsGoal }, |
| 193 |
{ label: surLabel, value: (surplus>=0?'+':'-') + cur(Math.abs(surplus)), color: surColor } |
| 194 |
].filter(function(c){ return !c.hide; }); |
| 195 |
|
| 196 |
summaryEl.innerHTML = cards.map(function(c) { |
| 197 |
return '<div class="bkbg-bp-sum-card" style="background:' + o.cardBg + ';border-left-color:' + c.color + '">' + |
| 198 |
'<div class="bkbg-bp-sum-label">' + c.label + '</div>' + |
| 199 |
'<div class="bkbg-bp-sum-value" style="color:' + c.color + '">' + c.value + '</div>' + |
| 200 |
'</div>'; |
| 201 |
}).join(''); |
| 202 |
|
| 203 |
// Update income display |
| 204 |
if (incomeDisp) incomeDisp.textContent = cur(income); |
| 205 |
if (savDisp) savDisp.textContent = cur(savingsGoal); |
| 206 |
|
| 207 |
// Allocation bar |
| 208 |
if (allocEl && o.showPieBreakdown) { |
| 209 |
var expPct = income > 0 ? Math.min(100, (totalExp/income)*100) : 0; |
| 210 |
var savPct = income > 0 ? Math.min(100-expPct, (sav/income)*100): 0; |
| 211 |
var surPct = Math.max(0, 100 - expPct - savPct); |
| 212 |
allocEl.innerHTML = |
| 213 |
'<div class="bkbg-bp-alloc-heading" style="color:' + o.labelColor + '">Income Allocation</div>' + |
| 214 |
'<div class="bkbg-bp-alloc-bar">' + |
| 215 |
'<div class="bkbg-bp-alloc-seg" style="width:' + expPct.toFixed(1) + '%;background:' + o.expenseColor + '"></div>' + |
| 216 |
'<div class="bkbg-bp-alloc-seg" style="width:' + savPct.toFixed(1) + '%;background:' + o.savingsColor + '"></div>' + |
| 217 |
'<div class="bkbg-bp-alloc-seg" style="width:' + surPct.toFixed(1) + '%;background:' + o.surplusColor + '"></div>' + |
| 218 |
'</div>' + |
| 219 |
'<div class="bkbg-bp-alloc-legend">' + |
| 220 |
'<span style="color:' + o.expenseColor + '">● Expenses ' + Math.round(expPct) + '%</span>' + |
| 221 |
'<span style="color:' + o.savingsColor + '">● Savings ' + Math.round(savPct) + '%</span>' + |
| 222 |
'<span style="color:' + o.surplusColor + '">● Surplus ' + Math.round(surPct) + '%</span>' + |
| 223 |
'</div>'; |
| 224 |
} |
| 225 |
} |
| 226 |
|
| 227 |
function escHtml(s) { |
| 228 |
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); |
| 229 |
} |
| 230 |
|
| 231 |
// ── Event bindings ───────────────────────────────────────────── |
| 232 |
if (incomeIn) { |
| 233 |
incomeIn.addEventListener('input', function() { |
| 234 |
income = parseFloat(incomeIn.value) || 0; |
| 235 |
renderCategories(); |
| 236 |
renderSummary(); |
| 237 |
}); |
| 238 |
} |
| 239 |
if (savIn) { |
| 240 |
savIn.addEventListener('input', function() { |
| 241 |
savingsGoal = parseFloat(savIn.value) || 0; |
| 242 |
renderSummary(); |
| 243 |
}); |
| 244 |
} |
| 245 |
if (addBtn) { |
| 246 |
addBtn.addEventListener('click', function() { |
| 247 |
var name = newName ? newName.value.trim() : ''; |
| 248 |
var amt = newAmt ? parseFloat(newAmt.value) || 0 : 0; |
| 249 |
if (!name) { if (newName) newName.focus(); return; } |
| 250 |
categories.push({ name: name, amount: amt, icon: '📋' }); |
| 251 |
if (newName) newName.value = ''; |
| 252 |
if (newAmt) newAmt.value = ''; |
| 253 |
renderCategories(); |
| 254 |
renderSummary(); |
| 255 |
}); |
| 256 |
} |
| 257 |
|
| 258 |
// ── Initial render ───────────────────────────────────────────── |
| 259 |
renderCategories(); |
| 260 |
renderSummary(); |
| 261 |
}); |
| 262 |
})(); |
| 263 |
|