| 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 TextControl = wp.components.TextControl; |
| 13 |
var ToggleControl = wp.components.ToggleControl; |
| 14 |
|
| 15 |
var _tc, _tv; |
| 16 |
function getTypoControl() { return _tc || (_tc = window.bkbgTypographyControl); } |
| 17 |
function getTypoCssVars() { return _tv || (_tv = window.bkbgTypoCssVars); } |
| 18 |
|
| 19 |
function calcROI(investment, returned, years) { |
| 20 |
var netProfit = returned - investment; |
| 21 |
var roi = investment > 0 ? (netProfit / investment) * 100 : 0; |
| 22 |
var annualized = investment > 0 && years > 0 |
| 23 |
? (Math.pow(returned / investment, 1 / years) - 1) * 100 |
| 24 |
: 0; |
| 25 |
var payback = netProfit > 0 && investment > 0 |
| 26 |
? investment / (netProfit / years) |
| 27 |
: null; |
| 28 |
return { netProfit: netProfit, roi: roi, annualized: annualized, payback: payback }; |
| 29 |
} |
| 30 |
|
| 31 |
function fmt(n, cur) { |
| 32 |
return cur + n.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 }); |
| 33 |
} |
| 34 |
function fmtPct(n) { |
| 35 |
return (n >= 0 ? '+' : '') + n.toFixed(2) + '%'; |
| 36 |
} |
| 37 |
|
| 38 |
function ROIPreview(props) { |
| 39 |
var a = props.attributes; |
| 40 |
var accent = a.accentColor || '#6c3fb5'; |
| 41 |
var cur = a.currency || '$'; |
| 42 |
var posClr = a.positiveColor || '#10b981'; |
| 43 |
var negClr = a.negativeColor || '#ef4444'; |
| 44 |
|
| 45 |
var _inv = useState(a.defaultInvestment || 10000); var inv = _inv[0]; var setInv = _inv[1]; |
| 46 |
var _ret = useState(a.defaultReturn || 15000); var ret = _ret[0]; var setRet = _ret[1]; |
| 47 |
var _yrs = useState(a.defaultYears || 3); var yrs = _yrs[0]; var setYrs = _yrs[1]; |
| 48 |
|
| 49 |
var r = calcROI(parseFloat(inv)||0, parseFloat(ret)||0, parseFloat(yrs)||1); |
| 50 |
var positive = r.netProfit >= 0; |
| 51 |
var resultBg = positive ? (a.resultBg || accent) : negClr; |
| 52 |
|
| 53 |
var inputStyle = { |
| 54 |
padding:'10px 12px', borderRadius:(a.inputRadius||8)+'px', |
| 55 |
border:'1.5px solid '+(a.inputBorder||'#e5e7eb'), |
| 56 |
fontSize:'15px', fontFamily:'inherit', outline:'none', width:'100%' |
| 57 |
}; |
| 58 |
var lblStyle = { display:'block', fontSize:'12px', fontWeight:600, color:a.labelColor||'#374151', marginBottom:'5px', textTransform:'uppercase', letterSpacing:'.05em' }; |
| 59 |
var statStyle = { |
| 60 |
background:a.statBg||'#f3f4f6', border:'1px solid '+(a.statBorder||'#e5e7eb'), |
| 61 |
borderRadius:'10px', padding:'14px 12px', textAlign:'center', flex:'1' |
| 62 |
}; |
| 63 |
|
| 64 |
return el('div', {style:{paddingTop:(a.paddingTop||60)+'px',paddingBottom:(a.paddingBottom||60)+'px',background:a.sectionBg||undefined}}, |
| 65 |
el('div', {style:{background:a.cardBg,borderRadius:(a.cardRadius||16)+'px',padding:'36px 32px',maxWidth:(a.maxWidth||560)+'px',margin:'0 auto',boxShadow:'0 4px 24px rgba(0,0,0,.09)'}}, |
| 66 |
(a.showTitle||a.showSubtitle) && el('div', {style:{marginBottom:'24px'}}, |
| 67 |
a.showTitle && el('div', {className:'bkbg-roi-title',style:{color:a.titleColor}}, a.title), |
| 68 |
a.showSubtitle && el('div', {className:'bkbg-roi-subtitle',style:{color:a.subtitleColor}}, a.subtitle) |
| 69 |
), |
| 70 |
|
| 71 |
// Inputs |
| 72 |
el('div', {style:{display:'grid',gridTemplateColumns:'1fr 1fr',gap:'14px',marginBottom:'14px'}}, |
| 73 |
el('div', null, |
| 74 |
el('label', {style:lblStyle}, 'Initial Investment'), |
| 75 |
el('div', {style:{position:'relative'}}, |
| 76 |
el('span', {style:{position:'absolute',left:'10px',top:'50%',transform:'translateY(-50%)',fontSize:'15px',color:'#6b7280',pointerEvents:'none'}}, cur), |
| 77 |
el('input', {type:'number',value:inv,min:0,style:{...inputStyle,paddingLeft:'26px'},onChange:function(e){setInv(e.target.value);}}) |
| 78 |
) |
| 79 |
), |
| 80 |
el('div', null, |
| 81 |
el('label', {style:lblStyle}, 'Total Return'), |
| 82 |
el('div', {style:{position:'relative'}}, |
| 83 |
el('span', {style:{position:'absolute',left:'10px',top:'50%',transform:'translateY(-50%)',fontSize:'15px',color:'#6b7280',pointerEvents:'none'}}, cur), |
| 84 |
el('input', {type:'number',value:ret,min:0,style:{...inputStyle,paddingLeft:'26px'},onChange:function(e){setRet(e.target.value);}}) |
| 85 |
) |
| 86 |
) |
| 87 |
), |
| 88 |
el('div', {style:{marginBottom:'20px'}}, |
| 89 |
el('label', {style:lblStyle}, 'Investment Period (Years)'), |
| 90 |
el('input', {type:'number',value:yrs,min:0.1,step:0.5,style:inputStyle,onChange:function(e){setYrs(e.target.value);}}) |
| 91 |
), |
| 92 |
|
| 93 |
// ROI result card |
| 94 |
el('div', {style:{background:resultBg,borderRadius:'12px',padding:'22px 24px',marginBottom:'18px',textAlign:'center',color:a.resultColor||'#fff'}}, |
| 95 |
el('div', {style:{fontSize:'13px',fontWeight:600,opacity:.8,marginBottom:'4px',textTransform:'uppercase',letterSpacing:'.06em'}}, 'Return on Investment'), |
| 96 |
el('div', {style:{fontSize:(a.resultSize||48)+'px',fontWeight:800,lineHeight:1}}, fmtPct(r.roi)), |
| 97 |
el('div', {style:{fontSize:'15px',opacity:.85,marginTop:'6px'}}, 'Net Profit: '+fmt(r.netProfit,cur)) |
| 98 |
), |
| 99 |
|
| 100 |
// Stat cards |
| 101 |
a.showBreakdown && el('div', {style:{display:'flex',gap:'10px',marginBottom:'14px',flexWrap:'wrap'}}, |
| 102 |
el('div', {style:statStyle}, |
| 103 |
el('div', {style:{fontSize:'22px',fontWeight:700,color:a.statValueColor||'#111827'}}, fmt(parseFloat(inv)||0,cur)), |
| 104 |
el('div', {style:{fontSize:'11px',color:a.statLabelColor||'#6b7280',marginTop:'3px'}}, 'Invested') |
| 105 |
), |
| 106 |
el('div', {style:statStyle}, |
| 107 |
el('div', {style:{fontSize:'22px',fontWeight:700,color:positive?posClr:negClr}}, fmt(r.netProfit,cur)), |
| 108 |
el('div', {style:{fontSize:'11px',color:a.statLabelColor||'#6b7280',marginTop:'3px'}}, 'Net Profit') |
| 109 |
), |
| 110 |
el('div', {style:statStyle}, |
| 111 |
el('div', {style:{fontSize:'22px',fontWeight:700,color:a.statValueColor||'#111827'}}, fmt(parseFloat(ret)||0,cur)), |
| 112 |
el('div', {style:{fontSize:'11px',color:a.statLabelColor||'#6b7280',marginTop:'3px'}}, 'Total Return') |
| 113 |
) |
| 114 |
), |
| 115 |
|
| 116 |
el('div', {style:{display:'flex',gap:'10px',flexWrap:'wrap'}}, |
| 117 |
a.showAnnualized && el('div', {style:{...statStyle,flex:'1'}}, |
| 118 |
el('div', {style:{fontSize:'22px',fontWeight:700,color:positive?posClr:negClr}}, fmtPct(r.annualized)), |
| 119 |
el('div', {style:{fontSize:'11px',color:a.statLabelColor||'#6b7280',marginTop:'3px'}}, 'Annualized ROI') |
| 120 |
), |
| 121 |
a.showPayback && el('div', {style:{...statStyle,flex:'1'}}, |
| 122 |
el('div', {style:{fontSize:'22px',fontWeight:700,color:a.statValueColor||'#111827'}}, |
| 123 |
r.payback != null ? r.payback.toFixed(1)+' yr' : '—'), |
| 124 |
el('div', {style:{fontSize:'11px',color:a.statLabelColor||'#6b7280',marginTop:'3px'}}, 'Payback Period') |
| 125 |
) |
| 126 |
) |
| 127 |
) |
| 128 |
); |
| 129 |
} |
| 130 |
|
| 131 |
registerBlockType('blockenberg/roi-calculator', { |
| 132 |
edit: function(props) { |
| 133 |
var a = props.attributes; var set = props.setAttributes; |
| 134 |
var blockProps = useBlockProps((function () { |
| 135 |
var _tv = getTypoCssVars(); |
| 136 |
var s = {}; |
| 137 |
if (_tv) { |
| 138 |
Object.assign(s, _tv(a.titleTypo, '--bkroi-tt-')); |
| 139 |
Object.assign(s, _tv(a.subtitleTypo, '--bkroi-st-')); |
| 140 |
} |
| 141 |
return { className: 'bkbg-roi-wrap', style: s }; |
| 142 |
})()); |
| 143 |
var colorSettings = [ |
| 144 |
{value:a.accentColor, onChange:function(v){set({accentColor:v});}, label:'Accent Color'}, |
| 145 |
{value:a.cardBg, onChange:function(v){set({cardBg:v});}, label:'Card Background'}, |
| 146 |
{value:a.resultBg, onChange:function(v){set({resultBg:v});}, label:'Result Card Background'}, |
| 147 |
{value:a.resultColor, onChange:function(v){set({resultColor:v});}, label:'Result Card Text'}, |
| 148 |
{value:a.positiveColor, onChange:function(v){set({positiveColor:v});}, label:'Positive / Profit Color'}, |
| 149 |
{value:a.negativeColor, onChange:function(v){set({negativeColor:v});}, label:'Negative / Loss Color'}, |
| 150 |
{value:a.statBg, onChange:function(v){set({statBg:v});}, label:'Stat Card Background'}, |
| 151 |
{value:a.statBorder, onChange:function(v){set({statBorder:v});}, label:'Stat Card Border'}, |
| 152 |
{value:a.statValueColor,onChange:function(v){set({statValueColor:v});}, label:'Stat Value Color'}, |
| 153 |
{value:a.statLabelColor,onChange:function(v){set({statLabelColor:v});}, label:'Stat Label Color'}, |
| 154 |
{value:a.inputBorder, onChange:function(v){set({inputBorder:v});}, label:'Input Border'}, |
| 155 |
{value:a.labelColor, onChange:function(v){set({labelColor:v});}, label:'Label Color'}, |
| 156 |
{value:a.titleColor, onChange:function(v){set({titleColor:v});}, label:'Title Color'}, |
| 157 |
{value:a.subtitleColor, onChange:function(v){set({subtitleColor:v});}, label:'Subtitle Color'}, |
| 158 |
{value:a.sectionBg, onChange:function(v){set({sectionBg:v});}, label:'Section Background'} |
| 159 |
]; |
| 160 |
return el(Fragment, null, |
| 161 |
el(InspectorControls, null, |
| 162 |
el(PanelBody,{title:'Header',initialOpen:false}, |
| 163 |
el(ToggleControl,{__nextHasNoMarginBottom:true,label:'Show Title', checked:a.showTitle, onChange:function(v){set({showTitle:v});}}), |
| 164 |
el(ToggleControl,{__nextHasNoMarginBottom:true,label:'Show Subtitle',checked:a.showSubtitle,onChange:function(v){set({showSubtitle:v});}}), |
| 165 |
el(TextControl,{label:'Title', value:a.title, onChange:function(v){set({title:v});}}), |
| 166 |
el(TextControl,{label:'Subtitle',value:a.subtitle,onChange:function(v){set({subtitle:v});}}) |
| 167 |
), |
| 168 |
el(PanelBody,{title:'Calculator Defaults',initialOpen:true}, |
| 169 |
el(TextControl,{label:'Currency Symbol',value:a.currency,onChange:function(v){set({currency:v});}}), |
| 170 |
el(TextControl,{label:'Default Investment',value:String(a.defaultInvestment),type:'number',onChange:function(v){set({defaultInvestment:parseFloat(v)||0});}}), |
| 171 |
el(TextControl,{label:'Default Total Return',value:String(a.defaultReturn),type:'number',onChange:function(v){set({defaultReturn:parseFloat(v)||0});}}), |
| 172 |
el(TextControl,{label:'Default Years',value:String(a.defaultYears),type:'number',onChange:function(v){set({defaultYears:parseFloat(v)||1});}}), |
| 173 |
el(ToggleControl,{__nextHasNoMarginBottom:true,label:'Show Payback Period', checked:a.showPayback, onChange:function(v){set({showPayback:v});}}), |
| 174 |
el(ToggleControl,{__nextHasNoMarginBottom:true,label:'Show Annualized Return', checked:a.showAnnualized, onChange:function(v){set({showAnnualized:v});}}), |
| 175 |
el(ToggleControl,{__nextHasNoMarginBottom:true,label:'Show Breakdown Cards', checked:a.showBreakdown, onChange:function(v){set({showBreakdown:v});}}) |
| 176 |
), |
| 177 |
|
| 178 |
el(PanelBody, { title: __('Typography', 'blockenberg'), initialOpen: false }, |
| 179 |
getTypoControl() && el(getTypoControl(), { |
| 180 |
label: __('Title Typography', 'blockenberg'), |
| 181 |
value: a.titleTypo || {}, |
| 182 |
onChange: function (v) { set({ titleTypo: v }); } |
| 183 |
}), |
| 184 |
getTypoControl() && el(getTypoControl(), { |
| 185 |
label: __('Subtitle Typography', 'blockenberg'), |
| 186 |
value: a.subtitleTypo || {}, |
| 187 |
onChange: function (v) { set({ subtitleTypo: v }); } |
| 188 |
}), |
| 189 |
el(RangeControl, { label: 'ROI Result Size', value: a.resultSize, min: 24, max: 80, step: 2, onChange: function (v) { set({ resultSize: v }); } }) |
| 190 |
), |
| 191 |
el(PanelColorSettings,{title:'Colors',initialOpen:false,colorSettings:colorSettings}), |
| 192 |
el(PanelBody,{title:'Sizing & Layout',initialOpen:false}, |
| 193 |
el(RangeControl,{label:'Card Border Radius',value:a.cardRadius, min:0,max:40,step:1, onChange:function(v){set({cardRadius:v});}}), |
| 194 |
el(RangeControl,{label:'Input Border Radius',value:a.inputRadius,min:0,max:20,step:1, onChange:function(v){set({inputRadius:v});}}), |
| 195 |
el(RangeControl,{label:'Max Width (px)', value:a.maxWidth, min:340,max:960,step:10,onChange:function(v){set({maxWidth:v});}}), |
| 196 |
el(RangeControl,{label:'Padding Top (px)', value:a.paddingTop, min:0,max:160,step:4, onChange:function(v){set({paddingTop:v});}}), |
| 197 |
el(RangeControl,{label:'Padding Bottom (px)',value:a.paddingBottom,min:0,max:160,step:4,onChange:function(v){set({paddingBottom:v});}}) |
| 198 |
) |
| 199 |
), |
| 200 |
el('div', blockProps, el(ROIPreview, {attributes:a})) |
| 201 |
); |
| 202 |
}, |
| 203 |
save: function(props) { |
| 204 |
var a = props.attributes; |
| 205 |
return el('div', wp.blockEditor.useBlockProps.save(), |
| 206 |
el('div', {className:'bkbg-roi-app','data-opts':JSON.stringify(a)})); |
| 207 |
} |
| 208 |
}); |
| 209 |
}() ); |
| 210 |
|