PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.10
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.10
2.0.14 2.0.13 2.0.12 2.0.11 2.0.10 2.0.9 trunk 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8
blockenberg / blocks / mortgage-calculator / frontend.js

frontend.js in Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor 2.0.10, at blocks/mortgage-calculator/frontend.js

272 lines 14.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 TERM_OPTIONS = [
26 { value: 10, label: '10 years' }, { value: 15, label: '15 years' },
27 { value: 20, label: '20 years' }, { value: 25, label: '25 years' },
28 { value: 30, label: '30 years' },
29 ];
30
31 function calcMortgage(price, downPct, rate, years) {
32 var down = price * downPct / 100;
33 var principal = price - down;
34 var n = years * 12;
35 var r = rate / 100 / 12;
36 var monthly = r === 0 ? principal / n : principal * r * Math.pow(1+r,n) / (Math.pow(1+r,n)-1);
37 var totalPaid = monthly * n;
38 var totalInterest = totalPaid - principal;
39 var rows = [], balance = principal;
40 for (var y = 1; y <= years; y++) {
41 var yp = 0, yi = 0;
42 for (var m = 0; m < 12; m++) {
43 var ip = balance * r, pp = monthly - ip;
44 yi += ip; yp += pp; balance -= pp;
45 }
46 rows.push({ year: y, principal: yp, interest: yi, balance: Math.max(0, balance) });
47 }
48 return { monthly: monthly, totalPaid: totalPaid, totalInterest: totalInterest, principal: principal, down: down, rows: rows };
49 }
50
51 function fmt(val, cur, pos) {
52 return pos === 'after' ? Math.round(val).toLocaleString('en-US') + ' ' + cur : cur + Math.round(val).toLocaleString('en-US');
53 }
54
55 function buildApp(app) {
56 var opts = {};
57 try { opts = JSON.parse(app.getAttribute('data-opts') || '{}'); } catch (e) {}
58
59 var accent = opts.accentColor || '#6c3fb5';
60 var cardBg = opts.cardBg || '#ffffff';
61 var resultBg = opts.resultBg || '#f5f3ff';
62 var resultBdr = opts.resultBorder || '#ede9fe';
63 var totalClr = opts.totalColor || accent;
64 var priClr = opts.principalColor || '#3b82f6';
65 var intClr = opts.interestColor || '#ef4444';
66 var pmiClr = opts.pmiColor || '#f59e0b';
67 var lclr = opts.labelColor || '#374151';
68 var tableBg = opts.tableBg || '#fafafa';
69 var cRadius = (opts.cardRadius || 16) + 'px';
70 var iRadius = (opts.inputRadius || 8) + 'px';
71 var maxW = (opts.maxWidth || 680) + 'px';
72 var ptop = (opts.paddingTop || 60) + 'px';
73 var pbot = (opts.paddingBottom || 60) + 'px';
74 var cur = opts.currency || '$';
75 var curPos = opts.currencyPos || 'before';
76 var fmtM = function (v) { return fmt(v, cur, curPos); };
77
78 var state = {
79 price: opts.defaultPrice || 350000,
80 down: opts.defaultDown || 20,
81 rate: opts.defaultRate || 6.5,
82 term: opts.defaultTerm || 30,
83 };
84
85 /* Card */
86 var card = document.createElement('div');
87 card.className = 'bkbg-mtg-card';
88 card.style.cssText = 'background:' + cardBg + ';border-radius:' + cRadius + ';max-width:' + maxW + ';margin:0 auto;padding:' + ptop + ' 32px ' + pbot;
89 app.innerHTML = '';
90 app.appendChild(card);
91
92 /* Title */
93 if (opts.showTitle !== false && opts.title) {
94 var h2 = document.createElement('h2');
95 h2.className = 'bkbg-mtg-title';
96 h2.style.cssText = 'color:' + (opts.titleColor||'#1e1b4b') + ';margin:0 0 8px;text-align:center';
97 h2.textContent = opts.title;
98 card.appendChild(h2);
99 }
100 if (opts.showSubtitle !== false && opts.subtitle) {
101 var sub = document.createElement('p');
102 sub.className = 'bkbg-mtg-subtitle';
103 sub.style.cssText = 'color:' + (opts.subtitleColor||'#6b7280') + ';text-align:center;margin:0 0 24px';
104 sub.textContent = opts.subtitle;
105 card.appendChild(sub);
106 }
107
108 /* Helpers */
109 var inpCSS = 'width:100%;padding:10px 12px;border:1.5px solid #e5e7eb;border-radius:' + iRadius + ';font-size:15px;box-sizing:border-box;outline:none;background:#fff;color:' + lclr;
110
111 function mkLabel(txt) {
112 var l = document.createElement('label');
113 l.style.cssText = 'font-size:13px;font-weight:600;color:' + lclr + ';display:block;margin-bottom:4px';
114 l.textContent = txt;
115 return l;
116 }
117 function mkField(label, inp) {
118 var d = document.createElement('div');
119 d.style.marginBottom = '14px';
120 d.appendChild(mkLabel(label));
121 d.appendChild(inp);
122 return d;
123 }
124 function mkNum(val, min, max, step, onChange) {
125 var i = document.createElement('input');
126 i.type = 'number'; i.value = val; i.min = min; i.max = max; i.step = step; i.style.cssText = inpCSS;
127 i.addEventListener('input', function () { onChange(parseFloat(i.value)||0); });
128 return i;
129 }
130
131 /* Inputs */
132 card.appendChild(mkField('Home Price (' + cur + ')', mkNum(state.price, 10000, 10000000, 1000, function(v){state.price=v; update();})));
133 card.appendChild(mkField('Down Payment (%)', mkNum(state.down, 0, 99, 0.5, function(v){state.down=Math.min(99,Math.max(0,v)); update();})));
134 card.appendChild(mkField('Annual Interest Rate (%)', mkNum(state.rate, 0.1, 30, 0.1, function(v){state.rate=v; update();})));
135
136 var termSel = document.createElement('select');
137 termSel.style.cssText = inpCSS + ';appearance:auto';
138 TERM_OPTIONS.forEach(function (o) {
139 var opt = document.createElement('option');
140 opt.value = o.value; opt.textContent = o.label;
141 if (o.value === state.term) opt.selected = true;
142 termSel.appendChild(opt);
143 });
144 termSel.addEventListener('change', function () { state.term = parseInt(termSel.value); update(); });
145 card.appendChild(mkField('Loan Term', termSel));
146
147 /* Result */
148 var resultBox = document.createElement('div');
149 resultBox.className = 'bkbg-mtg-result';
150 resultBox.style.cssText = 'background:' + resultBg + ';border:1.5px solid ' + resultBdr + ';border-radius:' + cRadius + ';padding:24px;text-align:center;margin-bottom:20px';
151 var rLbl = document.createElement('div');
152 rLbl.style.cssText = 'font-size:13px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.06em;margin-bottom:4px';
153 rLbl.textContent = 'Monthly Payment';
154 var rNum = document.createElement('div');
155 rNum.className = 'bkbg-mtg-monthly';
156 rNum.style.cssText = 'color:' + totalClr + ';margin-bottom:4px';
157 var rSub = document.createElement('div');
158 rSub.style.cssText = 'font-size:13px;color:#9ca3af';
159 var rPMI = document.createElement('div');
160 rPMI.className = 'bkbg-mtg-pmi-note';
161 rPMI.style.cssText = 'margin-top:8px;font-size:13px;color:' + pmiClr;
162 resultBox.appendChild(rLbl); resultBox.appendChild(rNum); resultBox.appendChild(rSub); resultBox.appendChild(rPMI);
163 card.appendChild(resultBox);
164
165 /* Breakdown */
166 var breakdown, stat1, stat2, stat3;
167 if (opts.showBreakdown !== false) {
168 breakdown = document.createElement('div');
169 breakdown.className = 'bkbg-mtg-breakdown';
170 breakdown.style.cssText = 'display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:20px';
171 function mkStat(lbl, color) {
172 var d = document.createElement('div');
173 d.style.cssText = 'text-align:center;background:#fff;border-radius:10px;padding:14px 12px;border:1.5px solid #e5e7eb';
174 d.innerHTML = '<div style="font-size:11px;text-transform:uppercase;letter-spacing:0.05em;color:#9ca3af;margin-bottom:4px">' + lbl + '</div>' +
175 '<div style="font-size:18px;font-weight:800;color:' + color + '">—</div>';
176 return d;
177 }
178 stat1 = mkStat('Loan Amount', priClr);
179 stat2 = mkStat('Total Interest', intClr);
180 stat3 = mkStat('Total Cost', totalClr);
181 breakdown.appendChild(stat1); breakdown.appendChild(stat2); breakdown.appendChild(stat3);
182 card.appendChild(breakdown);
183 }
184
185 /* LTV */
186 var ltvDiv;
187 if (opts.showLTV !== false) {
188 ltvDiv = document.createElement('div');
189 ltvDiv.className = 'bkbg-mtg-ltv';
190 ltvDiv.style.cssText = 'text-align:center;margin-bottom:20px;font-size:14px;color:' + lclr;
191 card.appendChild(ltvDiv);
192 }
193
194 /* Year table */
195 var tableWrap;
196 if (opts.showYearTable !== false) {
197 tableWrap = document.createElement('div');
198 tableWrap.className = 'bkbg-mtg-table-wrap';
199 tableWrap.style.overflowX = 'auto';
200 card.appendChild(tableWrap);
201 }
202
203 /* Update */
204 function update() {
205 var calc = calcMortgage(state.price, state.down, state.rate, state.term);
206 var ltv = Math.round(calc.principal / state.price * 100);
207 var showPMI = opts.showPMI !== false && state.down < 20;
208 var pmi = showPMI ? (calc.principal * (opts.pmiRate || 0.5) / 100 / 12) : 0;
209 var total = calc.monthly + pmi;
210
211 rNum.textContent = fmtM(total);
212 rSub.textContent = 'per month' + (showPMI ? ' (incl. PMI)' : '');
213 rPMI.style.display = showPMI ? '' : 'none';
214 if (showPMI) rPMI.textContent = 'PMI: ' + fmtM(pmi) + '/mo (down payment < 20%)';
215
216 if (breakdown) {
217 stat1.querySelector('div:last-child').textContent = fmtM(calc.principal);
218 stat2.querySelector('div:last-child').textContent = fmtM(calc.totalInterest);
219 stat3.querySelector('div:last-child').textContent = fmtM(calc.totalPaid);
220 }
221
222 if (ltvDiv) {
223 var ltvColor = ltv >= 80 ? intClr : priClr;
224 ltvDiv.innerHTML = 'Loan-to-Value (LTV): <strong style="color:' + ltvColor + '">' + ltv + '%</strong>' +
225 (ltv >= 80 ? ' — PMI may apply' : ' — no PMI required');
226 }
227
228 if (tableWrap) {
229 tableWrap.innerHTML = '';
230 var maxRows = opts.tableRows || 10;
231 var table = document.createElement('table');
232 table.className = 'bkbg-mtg-table';
233 table.style.cssText = 'width:100%;border-collapse:collapse;font-size:13px';
234 var thead = document.createElement('thead');
235 var hrow = document.createElement('tr');
236 hrow.style.background = tableBg;
237 ['Year','Balance','Principal Paid','Interest Paid'].forEach(function (h, i) {
238 var th = document.createElement('th');
239 th.textContent = h;
240 th.style.cssText = 'padding:10px 8px;font-weight:700;color:' + lclr + ';border-bottom:2px solid #e5e7eb;text-align:' + (i===0?'left':'right');
241 hrow.appendChild(th);
242 });
243 thead.appendChild(hrow);
244 table.appendChild(thead);
245 var tbody = document.createElement('tbody');
246 calc.rows.slice(0, maxRows).forEach(function (row, idx) {
247 var tr = document.createElement('tr');
248 var last = idx === Math.min(calc.rows.length, maxRows) - 1;
249 tr.style.cssText = 'background:' + (idx%2===0?'#fff':tableBg) + ';border-bottom:1px solid #f0f0f0;' + (last?'font-weight:700':'');
250 var td0 = document.createElement('td'); td0.style.cssText = 'padding:9px 8px;color:' + lclr; td0.textContent = 'Year ' + row.year;
251 var td1 = document.createElement('td'); td1.style.cssText = 'padding:9px 8px;text-align:right;color:' + totalClr; td1.textContent = fmtM(row.balance);
252 var td2 = document.createElement('td'); td2.style.cssText = 'padding:9px 8px;text-align:right;color:' + priClr; td2.textContent = fmtM(row.principal);
253 var td3 = document.createElement('td'); td3.style.cssText = 'padding:9px 8px;text-align:right;color:' + intClr; td3.textContent = fmtM(row.interest);
254 tr.appendChild(td0); tr.appendChild(td1); tr.appendChild(td2); tr.appendChild(td3);
255 tbody.appendChild(tr);
256 });
257 table.appendChild(tbody);
258 tableWrap.appendChild(table);
259 }
260 }
261
262 /* Typography CSS vars */
263 typoCssVarsForEl(app, opts.titleTypo, '--bkbg-mtg-tt-');
264 typoCssVarsForEl(app, opts.subtitleTypo, '--bkbg-mtg-st-');
265 typoCssVarsForEl(app, opts.resultTypo, '--bkbg-mtg-rs-');
266
267 update();
268 }
269
270 document.querySelectorAll('.bkbg-mtg-app').forEach(buildApp);
271 })();
272