PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.14
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.14
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 / debt-payoff-calculator / index.js

index.js in Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor 2.0.14, at blocks/debt-payoff-calculator/index.js

282 lines 20.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 ( function () {
2 var el = wp.element.createElement;
3 var useState = wp.element.useState;
4 var Fragment = wp.element.Fragment;
5 var registerBlockType = wp.blocks.registerBlockType;
6 var __ = wp.i18n.__;
7 var InspectorControls = wp.blockEditor.InspectorControls;
8 var PanelColorSettings = wp.blockEditor.PanelColorSettings;
9 var useBlockProps = wp.blockEditor.useBlockProps;
10 var PanelBody = wp.components.PanelBody;
11 var RangeControl = wp.components.RangeControl;
12 var SelectControl = wp.components.SelectControl;
13 var TextControl = wp.components.TextControl;
14 var ToggleControl = wp.components.ToggleControl;
15
16 var _dpcTC, _dpcTV;
17 function _tc() { return _dpcTC || (_dpcTC = window.bkbgTypographyControl); }
18 function _tv(t, p) { return (_dpcTV || (_dpcTV = window.bkbgTypoCssVars)) ? _dpcTV(t, p) : {}; }
19
20 var DEFAULT_DEBTS = [
21 {id:1, name:'Credit Card A', balance:5000, rate:22.99, minPay:100},
22 {id:2, name:'Credit Card B', balance:2800, rate:17.49, minPay:60},
23 {id:3, name:'Personal Loan', balance:8500, rate:11.5, minPay:180}
24 ];
25 var nextId = 10;
26
27 function calcPayoff(debts, extraMonthly, strategy) {
28 if (!debts.length) return {months:0, totalInterest:0, schedule:[]};
29 var list = debts.map(function(d){ return {name:d.name, balance:parseFloat(d.balance)||0, rate:(parseFloat(d.rate)||0)/100/12, min:parseFloat(d.minPay)||0}; });
30 list = list.filter(function(d){ return d.balance > 0; });
31 if (!list.length) return {months:0, totalInterest:0, schedule:[]};
32
33 var minTotal = list.reduce(function(s,d){ return s+d.min; }, 0);
34 var budget = minTotal + (extraMonthly||0);
35
36 // sort by strategy
37 var sorted = list.slice().sort(function(a,b){
38 return strategy === 'avalanche' ? b.rate - a.rate : a.balance - b.balance;
39 });
40
41 var schedule = [];
42 var totalInterest = 0;
43 var months = 0;
44 var MAX = 600;
45
46 while (sorted.some(function(d){ return d.balance > 0; }) && months < MAX) {
47 months++;
48 var available = budget;
49 var row = {month: months, debts:[], totalBalance:0};
50
51 // pay minimums first
52 sorted.forEach(function(d){
53 if (d.balance <= 0) return;
54 var interest = d.balance * d.rate;
55 totalInterest += interest;
56 d.balance += interest;
57 var pay = Math.min(d.min, d.balance);
58 d.balance -= pay;
59 available -= pay;
60 if (d.balance < 0.01) d.balance = 0;
61 });
62
63 // apply extra to priority debt
64 for (var i=0; i<sorted.length; i++) {
65 if (sorted[i].balance > 0 && available > 0) {
66 var extra = Math.min(available, sorted[i].balance);
67 sorted[i].balance -= extra;
68 available -= extra;
69 if (sorted[i].balance < 0.01) sorted[i].balance = 0;
70 }
71 }
72
73 sorted.forEach(function(d){ row.totalBalance += d.balance; });
74 row.totalInterest = Math.round(totalInterest*100)/100;
75 if (schedule.length < 120) schedule.push(row);
76 }
77
78 return {months: months, totalInterest: Math.round(totalInterest*100)/100, schedule: schedule};
79 }
80
81 function fmt(n, cur) { return (cur||'$') + Math.abs(n).toLocaleString('en', {minimumFractionDigits:2,maximumFractionDigits:2}); }
82
83 function DPCPreview(props) {
84 var a = props.attributes;
85 var accent = a.accentColor || '#6c3fb5';
86 var cur = a.currency || '$';
87
88 var _debts = useState(DEFAULT_DEBTS.map(function(d){ return Object.assign({},d); }));
89 var debts = _debts[0]; var setDebts = _debts[1];
90
91 var _extra = useState(200);
92 var extra = _extra[0]; var setExtra = _extra[1];
93
94 var _strat = useState(a.defaultStrategy || 'avalanche');
95 var strat = _strat[0]; var setStrat = _strat[1];
96
97 var avResult = calcPayoff(debts, extra, 'avalanche');
98 var sbResult = calcPayoff(debts, extra, 'snowball');
99 var curResult = strat === 'avalanche' ? avResult : sbResult;
100
101 var inpStyle = {padding:'6px 8px',border:'1.5px solid '+(a.inputBorder||'#e5e7eb'),borderRadius:(a.inputRadius||8)+'px',fontSize:'14px',fontFamily:'inherit',outline:'none',width:'100%',boxSizing:'border-box'};
102 var lblStyle = {fontSize:'12px',fontWeight:600,color:a.labelColor||'#374151',display:'block',marginBottom:'3px'};
103
104 function updateDebt(id, field, val) {
105 setDebts(debts.map(function(d){ return d.id===id ? Object.assign({},d,{[field]:val}) : d; }));
106 }
107
108 return el('div',{style:{paddingTop:(a.paddingTop||60)+'px',paddingBottom:(a.paddingBottom||60)+'px',background:a.sectionBg||undefined}},
109 el('div',{style:{background:a.cardBg||'#fff',borderRadius:(a.cardRadius||16)+'px',padding:'28px',maxWidth:(a.maxWidth||700)+'px',margin:'0 auto',boxShadow:'0 4px 24px rgba(0,0,0,.09)'}},
110
111 (a.showTitle||a.showSubtitle) && el('div',{style:{marginBottom:'20px'}},
112 a.showTitle && el('div',{className:'bkbg-dpc-title',style:{color:a.titleColor,marginBottom:'4px'}},(a.title)),
113 a.showSubtitle && el('div',{className:'bkbg-dpc-subtitle',style:{color:a.subtitleColor}},(a.subtitle))
114 ),
115
116 // Strategy tabs
117 el('div',{style:{display:'flex',gap:'8px',marginBottom:'20px'}},
118 [{id:'avalanche',label:'⬇ Avalanche (highest rate first)',color:a.avalancheColor||'#8b5cf6'},
119 {id:'snowball', label:'⛄ Snowball (lowest balance first)',color:a.snowballColor||'#3b82f6'}].map(function(s){
120 var active = strat===s.id;
121 return el('button',{key:s.id,onClick:function(){setStrat(s.id);},style:{flex:1,padding:'8px 12px',border:'2px solid '+(active?s.color:'#e5e7eb'),borderRadius:'8px',background:active?s.color+'18':'#fff',color:active?s.color:'#6b7280',fontWeight:active?700:500,cursor:'pointer',fontSize:'13px',fontFamily:'inherit',transition:'all .2s'}},s.label);
122 })
123 ),
124
125 // Debt rows header
126 el('div',{style:{display:'grid',gridTemplateColumns:'2fr 1fr 1fr 1fr 36px',gap:'6px',padding:'4px 8px',marginBottom:'4px'}},
127 ['Name','Balance','APR %','Min Pay',''].map(function(h){
128 return el('div',{key:h,style:lblStyle},h);
129 })
130 ),
131
132 debts.map(function(d){
133 return el('div',{key:d.id,style:{display:'grid',gridTemplateColumns:'2fr 1fr 1fr 1fr 36px',gap:'6px',alignItems:'center',background:a.rowBg||'#f9fafb',border:'1px solid '+(a.rowBorder||'#e5e7eb'),borderRadius:'8px',padding:'7px 8px',marginBottom:'6px'}},
134 el('input',{type:'text',value:d.name,placeholder:'Debt name',style:inpStyle,onChange:function(e){updateDebt(d.id,'name',e.target.value);}}),
135 el('input',{type:'number',value:d.balance,min:0,style:inpStyle,onChange:function(e){updateDebt(d.id,'balance',e.target.value);}}),
136 el('input',{type:'number',value:d.rate,min:0,max:100,step:0.01,style:inpStyle,onChange:function(e){updateDebt(d.id,'rate',e.target.value);}}),
137 el('input',{type:'number',value:d.minPay,min:0,style:inpStyle,onChange:function(e){updateDebt(d.id,'minPay',e.target.value);}}),
138 el('button',{onClick:function(){setDebts(debts.filter(function(x){return x.id!==d.id;}));},style:{border:'none',background:'#fee2e2',color:'#ef4444',borderRadius:'6px',cursor:'pointer',padding:'4px 8px',fontWeight:700,fontSize:'14px',fontFamily:'inherit',width:'36px',height:'32px'}},'×')
139 );
140 }),
141
142 el('div',{style:{display:'flex',gap:'12px',marginTop:'8px',marginBottom:'20px',flexWrap:'wrap',alignItems:'flex-end'}},
143 el('button',{onClick:function(){nextId++;setDebts(debts.concat({id:nextId,name:'New Debt',balance:1000,rate:15,minPay:25}));},style:{padding:'8px 16px',background:accent,color:'#fff',border:'none',borderRadius:'8px',fontWeight:700,cursor:'pointer',fontSize:'13px',fontFamily:'inherit'}},'+ Add Debt'),
144 el('div',{style:{flex:1,minWidth:'160px'}},
145 el('label',{style:lblStyle},'Extra Monthly Payment ('+cur+')'),
146 el('input',{type:'number',value:extra,min:0,style:{...inpStyle,maxWidth:'180px'},onChange:function(e){setExtra(parseFloat(e.target.value)||0);}})
147 )
148 ),
149
150 // Results
151 el('div',{style:{background:a.resultBg||'#f5f3ff',border:'1px solid '+(a.resultBorder||'#ede9fe'),borderRadius:'12px',padding:'18px',marginBottom:'16px'}},
152 el('div',{style:{fontSize:'13px',fontWeight:700,color:strat==='avalanche'?(a.avalancheColor||'#8b5cf6'):(a.snowballColor||'#3b82f6'),textTransform:'uppercase',letterSpacing:'.05em',marginBottom:'12px'}},
153 (strat==='avalanche'?'Avalanche':'Snowball')+' Strategy Results'
154 ),
155 el('div',{style:{display:'grid',gridTemplateColumns:'repeat(3,1fr)',gap:'12px'}},
156 [{label:'Months to Payoff',val:curResult.months+' mo'},
157 {label:'Years to Payoff', val:(curResult.months/12).toFixed(1)+' yr'},
158 {label:'Total Interest', val:fmt(curResult.totalInterest,cur)}
159 ].map(function(item){
160 return el('div',{key:item.label,style:{textAlign:'center',background:'#fff',borderRadius:'8px',padding:'12px 8px'}},
161 el('div',{style:{fontSize:(a.resultNumFontSize||22)+'px',fontWeight:800,color:accent}},(item.val)),
162 el('div',{style:{fontSize:'12px',color:'#6b7280',marginTop:'2px'}},(item.label))
163 );
164 })
165 )
166 ),
167
168 // Comparison
169 a.showComparison && el('div',{style:{background:'#fff',border:'1px solid #e5e7eb',borderRadius:'12px',padding:'16px',marginBottom:'16px'}},
170 el('div',{style:{fontSize:'13px',fontWeight:700,color:'#374151',textTransform:'uppercase',letterSpacing:'.05em',marginBottom:'12px'}},'Strategy Comparison'),
171 el('div',{style:{display:'grid',gridTemplateColumns:'1fr 1fr',gap:'12px'}},
172 [{label:'Avalanche',res:avResult,color:a.avalancheColor||'#8b5cf6'},
173 {label:'Snowball', res:sbResult, color:a.snowballColor||'#3b82f6'}].map(function(s){
174 return el('div',{key:s.label,style:{background:s.color+'0d',border:'2px solid '+s.color+'44',borderRadius:'10px',padding:'12px',textAlign:'center'}},
175 el('div',{style:{color:s.color,fontWeight:800,fontSize:(a.stratLabelFontSize||15)+'px',marginBottom:'4px'}},s.label),
176 el('div',{style:{fontSize:'20px',fontWeight:800,color:'#1f2937'}},s.res.months+' mo'),
177 el('div',{style:{fontSize:'13px',color:'#6b7280'}},'Interest: '+fmt(s.res.totalInterest,cur))
178 );
179 })
180 ),
181 avResult.months > 0 && sbResult.months > 0 && el('div',{style:{marginTop:'12px',textAlign:'center',fontSize:'14px',color:'#374151',background:'#f9fafb',borderRadius:'8px',padding:'10px'}},
182 avResult.totalInterest <= sbResult.totalInterest
183 ? '💡 Avalanche saves '+fmt(sbResult.totalInterest-avResult.totalInterest,cur)+' in interest vs. Snowball'
184 : '💡 Snowball saves '+fmt(avResult.totalInterest-sbResult.totalInterest,cur)+' in interest vs. Avalanche'
185 )
186 ),
187
188 // Schedule preview
189 a.showSchedule && curResult.schedule.length > 0 && el('div',null,
190 el('div',{style:{fontSize:'13px',fontWeight:700,color:'#374151',textTransform:'uppercase',letterSpacing:'.05em',marginBottom:'8px'}},'Payoff Schedule (first '+(a.scheduleRows||12)+' months)'),
191 el('div',{style:{overflowX:'auto'}},
192 el('table',{style:{width:'100%',borderCollapse:'collapse',fontSize:(a.tableFontSize||13)+'px'}},
193 el('thead',null,
194 el('tr',null,
195 ['Month','Remaining Balance','Total Interest Paid'].map(function(h){
196 return el('th',{key:h,style:{textAlign:'left',padding:'6px 10px',background:'#f3f4f6',color:'#374151',fontWeight:600,borderBottom:'2px solid #e5e7eb',whiteSpace:'nowrap'}},h);
197 })
198 )
199 ),
200 el('tbody',null,
201 curResult.schedule.slice(0,a.scheduleRows||12).map(function(row,i){
202 return el('tr',{key:row.month,style:{background:i%2===0?'#fff':'#f9fafb'}},
203 el('td',{style:{padding:'6px 10px',color:'#374151',borderBottom:'1px solid #f3f4f6'}},row.month),
204 el('td',{style:{padding:'6px 10px',color:'#374151',fontWeight:600,borderBottom:'1px solid #f3f4f6'}},fmt(row.totalBalance,cur)),
205 el('td',{style:{padding:'6px 10px',color:'#ef4444',borderBottom:'1px solid #f3f4f6'}},fmt(row.totalInterest,cur))
206 );
207 })
208 )
209 )
210 )
211 )
212 )
213 );
214 }
215
216 registerBlockType('blockenberg/debt-payoff-calculator', {
217 edit: function(props) {
218 var a = props.attributes; var set = props.setAttributes;
219 var blockProps = useBlockProps({ style: Object.assign(
220 { '--bkbg-dpc-ttl-fs': (a.titleSize || 28) + 'px',
221 '--bkbg-dpc-sub-fs': (a.subtitleFontSize || 15) + 'px' },
222 _tv(a.typoTitle || {}, '--bkbg-dpc-ttl-'),
223 _tv(a.typoSubtitle || {}, '--bkbg-dpc-sub-')
224 ) });
225 var colorSettings = [
226 {value:a.accentColor, onChange:function(v){set({accentColor:v});}, label:'Accent Color'},
227 {value:a.cardBg, onChange:function(v){set({cardBg:v});}, label:'Card Background'},
228 {value:a.avalancheColor, onChange:function(v){set({avalancheColor:v});}, label:'Avalanche Strategy Color'},
229 {value:a.snowballColor, onChange:function(v){set({snowballColor:v});}, label:'Snowball Strategy Color'},
230 {value:a.resultBg, onChange:function(v){set({resultBg:v});}, label:'Results Background'},
231 {value:a.resultBorder, onChange:function(v){set({resultBorder:v});}, label:'Results Border'},
232 {value:a.rowBg, onChange:function(v){set({rowBg:v});}, label:'Debt Row Background'},
233 {value:a.rowBorder, onChange:function(v){set({rowBorder:v});}, label:'Debt Row Border'},
234 {value:a.inputBorder, onChange:function(v){set({inputBorder:v});}, label:'Input Border'},
235 {value:a.labelColor, onChange:function(v){set({labelColor:v});}, label:'Label Color'},
236 {value:a.titleColor, onChange:function(v){set({titleColor:v});}, label:'Title Color'},
237 {value:a.subtitleColor, onChange:function(v){set({subtitleColor:v});}, label:'Subtitle Color'},
238 {value:a.sectionBg, onChange:function(v){set({sectionBg:v});}, label:'Section Background'}
239 ];
240 return el(Fragment,null,
241 el(InspectorControls,null,
242 el(PanelBody,{title:'Header',initialOpen:false},
243 el(ToggleControl,{__nextHasNoMarginBottom:true,label:'Show Title', checked:a.showTitle, onChange:function(v){set({showTitle:v});}}),
244 el(ToggleControl,{__nextHasNoMarginBottom:true,label:'Show Subtitle',checked:a.showSubtitle,onChange:function(v){set({showSubtitle:v});}}),
245 el(TextControl,{label:'Title', value:a.title, onChange:function(v){set({title:v});}}),
246 el(TextControl,{label:'Subtitle',value:a.subtitle,onChange:function(v){set({subtitle:v});}})
247 ),
248 el(PanelBody,{title:'Calculator Settings',initialOpen:true},
249 el(TextControl,{label:'Currency Symbol',value:a.currency,onChange:function(v){set({currency:v});}}),
250 el(SelectControl,{label:'Default Strategy',value:a.defaultStrategy,options:[{label:'Avalanche (highest rate first)',value:'avalanche'},{label:'Snowball (lowest balance first)',value:'snowball'}],onChange:function(v){set({defaultStrategy:v});}}),
251 el(ToggleControl,{__nextHasNoMarginBottom:true,label:'Show Strategy Comparison',checked:a.showComparison,onChange:function(v){set({showComparison:v});}}),
252 el(ToggleControl,{__nextHasNoMarginBottom:true,label:'Show Payment Schedule', checked:a.showSchedule, onChange:function(v){set({showSchedule:v});}}),
253 el(RangeControl,{label:'Schedule Rows to Show',value:a.scheduleRows,min:6,max:60,step:6,onChange:function(v){set({scheduleRows:v});}})
254 ),
255
256 el( PanelBody, { title: __( 'Typography', 'blockenberg' ), initialOpen: false },
257 _tc() && _tc()({ label: __('Title typography', 'blockenberg'), value: a.typoTitle || {}, onChange: function (v) { set({ typoTitle: v }); } }),
258 _tc() && _tc()({ label: __('Subtitle typography', 'blockenberg'), value: a.typoSubtitle || {}, onChange: function (v) { set({ typoSubtitle: v }); } }),
259 el(RangeControl,{label:'Result number size (px)',value:a.resultNumFontSize,min:14,max:36,step:1,onChange:function(v){set({resultNumFontSize:v});}, __nextHasNoMarginBottom:true}),
260 el(RangeControl,{label:'Strategy label size (px)',value:a.stratLabelFontSize,min:11,max:22,step:1,onChange:function(v){set({stratLabelFontSize:v});},__nextHasNoMarginBottom:true}),
261 el(RangeControl,{label:'Table font size (px)',value:a.tableFontSize, min:10,max:18,step:1, onChange:function(v){set({tableFontSize:v});}, __nextHasNoMarginBottom:true})
262 ),
263 el(PanelColorSettings,{title:'Colors',initialOpen:false,colorSettings:colorSettings}),
264 el(PanelBody,{title:'Sizing & Layout',initialOpen:false},
265 el(RangeControl,{label:'Card Border Radius',value:a.cardRadius, min:0, max:40,step:1, onChange:function(v){set({cardRadius:v});}}),
266 el(RangeControl,{label:'Input Radius', value:a.inputRadius, min:0, max:20,step:1, onChange:function(v){set({inputRadius:v});}}),
267 el(RangeControl,{label:'Max Width (px)', value:a.maxWidth, min:360,max:1100,step:10,onChange:function(v){set({maxWidth:v});}}),
268 el(RangeControl,{label:'Padding Top', value:a.paddingTop, min:0, max:160,step:4, onChange:function(v){set({paddingTop:v});}}),
269 el(RangeControl,{label:'Padding Bottom', value:a.paddingBottom,min:0,max:160,step:4, onChange:function(v){set({paddingBottom:v});}})
270 )
271 ),
272 el('div',blockProps, el(DPCPreview,{attributes:a}))
273 );
274 },
275 save: function(props){
276 var a = props.attributes;
277 return el('div',wp.blockEditor.useBlockProps.save(),
278 el('div',{className:'bkbg-dpc-app','data-opts':JSON.stringify(a)}));
279 }
280 });
281 }() );
282