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 / break-even-calculator / index.js

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

236 lines 17.6 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
8 function getTypographyControl() {
9 return (window.bkbgTypographyControl || function () { return null; });
10 }
11
12 function _tv(typo, prefix) {
13 if (!typo) return {};
14 var s = {};
15 if (typo.family) s[prefix + 'font-family'] = "'" + typo.family + "', sans-serif";
16 if (typo.weight) s[prefix + 'font-weight'] = typo.weight;
17 if (typo.transform) s[prefix + 'text-transform'] = typo.transform;
18 if (typo.style) s[prefix + 'font-style'] = typo.style;
19 if (typo.decoration) s[prefix + 'text-decoration'] = typo.decoration;
20 var su = typo.sizeUnit || 'px';
21 if (typo.sizeDesktop !== '' && typo.sizeDesktop != null) s[prefix + 'font-size-d'] = typo.sizeDesktop + su;
22 if (typo.sizeTablet !== '' && typo.sizeTablet != null) s[prefix + 'font-size-t'] = typo.sizeTablet + su;
23 if (typo.sizeMobile !== '' && typo.sizeMobile != null) s[prefix + 'font-size-m'] = typo.sizeMobile + su;
24 var lhu = typo.lineHeightUnit || '';
25 if (typo.lineHeightDesktop !== '' && typo.lineHeightDesktop != null) s[prefix + 'line-height-d'] = typo.lineHeightDesktop + lhu;
26 if (typo.lineHeightTablet !== '' && typo.lineHeightTablet != null) s[prefix + 'line-height-t'] = typo.lineHeightTablet + lhu;
27 if (typo.lineHeightMobile !== '' && typo.lineHeightMobile != null) s[prefix + 'line-height-m'] = typo.lineHeightMobile + lhu;
28 var lsu = typo.letterSpacingUnit || 'px';
29 if (typo.letterSpacingDesktop !== '' && typo.letterSpacingDesktop != null) s[prefix + 'letter-spacing-d'] = typo.letterSpacingDesktop + lsu;
30 if (typo.letterSpacingTablet !== '' && typo.letterSpacingTablet != null) s[prefix + 'letter-spacing-t'] = typo.letterSpacingTablet + lsu;
31 if (typo.letterSpacingMobile !== '' && typo.letterSpacingMobile != null) s[prefix + 'letter-spacing-m'] = typo.letterSpacingMobile + lsu;
32 var wsu = typo.wordSpacingUnit || 'px';
33 if (typo.wordSpacingDesktop !== '' && typo.wordSpacingDesktop != null) s[prefix + 'word-spacing-d'] = typo.wordSpacingDesktop + wsu;
34 if (typo.wordSpacingTablet !== '' && typo.wordSpacingTablet != null) s[prefix + 'word-spacing-t'] = typo.wordSpacingTablet + wsu;
35 if (typo.wordSpacingMobile !== '' && typo.wordSpacingMobile != null) s[prefix + 'word-spacing-m'] = typo.wordSpacingMobile + wsu;
36 return s;
37 }
38
39 var InspectorControls = wp.blockEditor.InspectorControls;
40 var PanelColorSettings = wp.blockEditor.PanelColorSettings;
41 var useBlockProps = wp.blockEditor.useBlockProps;
42 var PanelBody = wp.components.PanelBody;
43 var RangeControl = wp.components.RangeControl;
44 var SelectControl = wp.components.SelectControl;
45 var TextControl = wp.components.TextControl;
46 var ToggleControl = wp.components.ToggleControl;
47
48 function calcBEP(fixedCosts, pricePerUnit, varCostUnit, unitsToSell) {
49 var contribution = pricePerUnit - varCostUnit;
50 var bepUnits = contribution > 0 ? fixedCosts / contribution : Infinity;
51 var bepRevenue = bepUnits * pricePerUnit;
52 var profitAtUnits = unitsToSell * contribution - fixedCosts;
53 var marginRatio = pricePerUnit > 0 ? (contribution / pricePerUnit) * 100 : 0;
54 return {
55 bepUnits: Math.ceil(bepUnits),
56 bepRevenue: bepRevenue,
57 contribution: contribution,
58 profitAtUnits: profitAtUnits,
59 marginRatio: marginRatio,
60 revenue: unitsToSell * pricePerUnit,
61 totalCosts: fixedCosts + unitsToSell * varCostUnit
62 };
63 }
64
65 function fmt(n, cur) {
66 return cur + Math.abs(n).toLocaleString('en-US', {minimumFractionDigits:0,maximumFractionDigits:0});
67 }
68
69 function BEPPreview(props) {
70 var a = props.attributes;
71 var accent = a.accentColor || '#6c3fb5';
72 var profitClr = a.profitColor || '#10b981';
73 var lossClr = a.lossColor || '#ef4444';
74 var cur = a.currency || '$';
75
76 var _fixed = useState(a.defaultFixedCosts || 10000); var fixed = _fixed[0]; var setFixed = _fixed[1];
77 var _price = useState(a.defaultPricePerUnit || 50); var price = _price[0]; var setPrice = _price[1];
78 var _varCost = useState(a.defaultVarCostUnit || 20); var varCost = _varCost[0]; var setVarCost = _varCost[1];
79 var _units = useState(a.defaultUnitsToSell || 500); var units = _units[0]; var setUnits = _units[1];
80
81 var r = calcBEP(parseFloat(fixed)||0, parseFloat(price)||0, parseFloat(varCost)||0, parseFloat(units)||0);
82 var profitable = r.profitAtUnits >= 0;
83 var progress = r.bepUnits > 0 && isFinite(r.bepUnits)
84 ? Math.min(100, (parseFloat(units)/r.bepUnits)*100) : 0;
85
86 var inputStyle = {padding:'9px 12px',borderRadius:(a.inputRadius||8)+'px',border:'1.5px solid '+(a.inputBorder||'#e5e7eb'),fontSize:'14px',fontFamily:'inherit',outline:'none',width:'100%',background:'#fff'};
87 var lblStyle = {display:'block',fontSize:'12px',fontWeight:600,color:a.labelColor||'#374151',marginBottom:'5px',textTransform:'uppercase',letterSpacing:'.05em'};
88 var statStyle = {background:a.statBg||'#f3f4f6',border:'1px solid '+(a.statBorder||'#e5e7eb'),borderRadius:'10px',padding:'14px 12px',textAlign:'center',flex:1};
89
90 function CurrencyInput(lbl, val, setter) {
91 return el('div', null,
92 el('label', {style:lblStyle}, lbl),
93 el('div', {style:{position:'relative'}},
94 el('span', {style:{position:'absolute',left:'10px',top:'50%',transform:'translateY(-50%)',fontSize:'14px',color:'#6b7280',pointerEvents:'none'}}, cur),
95 el('input', {type:'number',value:val,min:0,style:{...inputStyle,paddingLeft:'26px'},onChange:function(e){setter(e.target.value);}})
96 )
97 );
98 }
99
100 return el('div', {style:{paddingTop:(a.paddingTop||60)+'px',paddingBottom:(a.paddingBottom||60)+'px',background:a.sectionBg||undefined}},
101 el('div', {style:{background:a.cardBg,borderRadius:(a.cardRadius||16)+'px',padding:'36px 32px',maxWidth:(a.maxWidth||580)+'px',margin:'0 auto',boxShadow:'0 4px 24px rgba(0,0,0,.09)'}},
102
103 (a.showTitle||a.showSubtitle) && el('div',{style:{marginBottom:'24px'}},
104 a.showTitle && el('div',{className:'bkbg-bep-title',style:{color:a.titleColor,marginBottom:'6px'}},a.title),
105 a.showSubtitle && el('div',{className:'bkbg-bep-subtitle',style:{color:a.subtitleColor}},a.subtitle)
106 ),
107
108 // Inputs grid
109 el('div',{style:{display:'grid',gridTemplateColumns:'1fr 1fr',gap:'12px',marginBottom:'12px'}},
110 el(CurrencyInput,'Fixed Costs', fixed, setFixed),
111 el('div',null, el('label',{style:lblStyle},'Units to Sell'),
112 el('input',{type:'number',value:units,min:0,style:inputStyle,onChange:function(e){setUnits(e.target.value);}}))
113 ),
114 el('div',{style:{display:'grid',gridTemplateColumns:'1fr 1fr',gap:'12px',marginBottom:'20px'}},
115 el(CurrencyInput,'Price per Unit', price, setPrice),
116 el(CurrencyInput,'Variable Cost / Unit',varCost, setVarCost)
117 ),
118
119 // BEP result card
120 el('div',{style:{background:a.resultBg||accent,borderRadius:'12px',padding:'20px 24px',textAlign:'center',color:a.resultColor||'#fff',marginBottom:'18px'}},
121 el('div',{style:{fontSize:'13px',fontWeight:600,opacity:.8,textTransform:'uppercase',letterSpacing:'.06em',marginBottom:'4px'}},'Break-Even Point'),
122 el('div',{style:{fontSize:(a.resultSize||46)+'px',fontWeight:800,lineHeight:1}},
123 isFinite(r.bepUnits) ? r.bepUnits.toLocaleString() + ' units' : '∞'),
124 el('div',{style:{fontSize:'15px',opacity:.85,marginTop:'6px'}},
125 isFinite(r.bepRevenue) ? 'Revenue needed: ' + fmt(r.bepRevenue, cur) : 'Check your inputs')
126 ),
127
128 // Progress toward BEP
129 a.showProfitZone && isFinite(r.bepUnits) && el('div',{style:{marginBottom:'18px'}},
130 el('div',{style:{display:'flex',justifyContent:'space-between',fontSize:'12px',marginBottom:'6px',fontWeight:600,color:a.labelColor||'#374151'}},
131 el('span',null,'0 units'),
132 el('span',{style:{color:profitable?profitClr:lossClr}},parseFloat(units).toLocaleString()+' units sold → '+(profitable?'PROFIT':'LOSS'))
133 ),
134 el('div',{style:{background:a.barBg||'#e5e7eb',borderRadius:'20px',height:'12px',overflow:'hidden',position:'relative'}},
135 el('div',{style:{width:progress+'%',height:'100%',background:profitable?profitClr:lossClr,borderRadius:'20px',transition:'width .4s ease'}}),
136 isFinite(r.bepUnits) && el('div',{style:{position:'absolute',top:0,left:'50%',transform:'translateX(-50%)',height:'100%',width:'2px',background:'rgba(0,0,0,.18)'}})
137 ),
138 el('div',{style:{textAlign:'center',marginTop:'5px',fontSize:'12px',color:'#6b7280'}},'↑ Break-even at '+r.bepUnits+' units')
139 ),
140
141 // Stat cards
142 el('div',{style:{display:'flex',gap:'10px',flexWrap:'wrap',marginBottom: (a.showContribution||a.showMarginRatio) ? '10px' : '0'}},
143 el('div',{style:statStyle},
144 el('div',{style:{fontSize:'20px',fontWeight:700,color:profitable?profitClr:lossClr}},
145 (r.profitAtUnits<0?'-':'')+fmt(r.profitAtUnits,cur)),
146 el('div',{style:{fontSize:'11px',color:'#6b7280',marginTop:'3px'}},
147 profitable?'Profit at '+units+' units':'Loss at '+units+' units')
148 ),
149 el('div',{style:statStyle},
150 el('div',{style:{fontSize:'20px',fontWeight:700,color:'#111827'}},fmt(r.revenue,cur)),
151 el('div',{style:{fontSize:'11px',color:'#6b7280',marginTop:'3px'}},'Total Revenue')
152 ),
153 el('div',{style:statStyle},
154 el('div',{style:{fontSize:'20px',fontWeight:700,color:'#111827'}},fmt(r.totalCosts,cur)),
155 el('div',{style:{fontSize:'11px',color:'#6b7280',marginTop:'3px'}},'Total Costs')
156 )
157 ),
158
159 (a.showContribution||a.showMarginRatio) && el('div',{style:{display:'flex',gap:'10px',flexWrap:'wrap'}},
160 a.showContribution && el('div',{style:statStyle},
161 el('div',{style:{fontSize:'20px',fontWeight:700,color:accent}},fmt(r.contribution,cur)),
162 el('div',{style:{fontSize:'11px',color:'#6b7280',marginTop:'3px'}},'Contribution Margin / Unit')
163 ),
164 a.showMarginRatio && el('div',{style:statStyle},
165 el('div',{style:{fontSize:'20px',fontWeight:700,color:accent}},r.marginRatio.toFixed(1)+'%'),
166 el('div',{style:{fontSize:'11px',color:'#6b7280',marginTop:'3px'}},'Contribution Margin Ratio')
167 )
168 )
169 )
170 );
171 }
172
173 registerBlockType('blockenberg/break-even-calculator', {
174 edit: function(props) {
175 var a = props.attributes; var set = props.setAttributes;
176 var blockProps = useBlockProps({ style: Object.assign({}, _tv(a.typoTitle, '--bkbg-bep-title-'), _tv(a.typoSubtitle, '--bkbg-bep-sub-')) });
177 var colorSettings = [
178 {value:a.accentColor, onChange:function(v){set({accentColor:v});}, label:'Accent Color'},
179 {value:a.cardBg, onChange:function(v){set({cardBg:v});}, label:'Card Background'},
180 {value:a.resultBg, onChange:function(v){set({resultBg:v});}, label:'Result Card Background'},
181 {value:a.resultColor, onChange:function(v){set({resultColor:v});}, label:'Result Card Text'},
182 {value:a.profitColor, onChange:function(v){set({profitColor:v});}, label:'Profit Color'},
183 {value:a.lossColor, onChange:function(v){set({lossColor:v});}, label:'Loss Color'},
184 {value:a.barBg, onChange:function(v){set({barBg:v});}, label:'Progress Bar Track'},
185 {value:a.statBg, onChange:function(v){set({statBg:v});}, label:'Stat Card Background'},
186 {value:a.statBorder, onChange:function(v){set({statBorder:v});}, label:'Stat Card Border'},
187 {value:a.inputBorder, onChange:function(v){set({inputBorder:v});}, label:'Input Border'},
188 {value:a.labelColor, onChange:function(v){set({labelColor:v});}, label:'Label Color'},
189 {value:a.titleColor, onChange:function(v){set({titleColor:v});}, label:'Title Color'},
190 {value:a.subtitleColor,onChange:function(v){set({subtitleColor:v});}, label:'Subtitle Color'},
191 {value:a.sectionBg, onChange:function(v){set({sectionBg:v});}, label:'Section Background'}
192 ];
193 return el(Fragment, null,
194 el(InspectorControls, null,
195 el(PanelBody,{title:'Header',initialOpen:false},
196 el(ToggleControl,{__nextHasNoMarginBottom:true,label:'Show Title', checked:a.showTitle, onChange:function(v){set({showTitle:v});}}),
197 el(ToggleControl,{__nextHasNoMarginBottom:true,label:'Show Subtitle',checked:a.showSubtitle,onChange:function(v){set({showSubtitle:v});}}),
198 el(TextControl,{label:'Title', value:a.title, onChange:function(v){set({title:v});}}),
199 el(TextControl,{label:'Subtitle',value:a.subtitle,onChange:function(v){set({subtitle:v});}})
200 ),
201 el(PanelBody,{title:'Calculator Defaults',initialOpen:true},
202 el(TextControl,{label:'Currency Symbol', value:a.currency, onChange:function(v){set({currency:v});}}),
203 el(TextControl,{label:'Default Fixed Costs',value:String(a.defaultFixedCosts), type:'number',onChange:function(v){set({defaultFixedCosts:parseFloat(v)||0});}}),
204 el(TextControl,{label:'Default Price/Unit', value:String(a.defaultPricePerUnit), type:'number',onChange:function(v){set({defaultPricePerUnit:parseFloat(v)||0});}}),
205 el(TextControl,{label:'Default Var Cost/Unit',value:String(a.defaultVarCostUnit),type:'number',onChange:function(v){set({defaultVarCostUnit:parseFloat(v)||0});}}),
206 el(TextControl,{label:'Default Units to Sell',value:String(a.defaultUnitsToSell),type:'number',onChange:function(v){set({defaultUnitsToSell:parseFloat(v)||0});}}),
207 el(ToggleControl,{__nextHasNoMarginBottom:true,label:'Show Profit Zone Bar', checked:a.showProfitZone, onChange:function(v){set({showProfitZone:v});}}),
208 el(ToggleControl,{__nextHasNoMarginBottom:true,label:'Show Contribution Margin', checked:a.showContribution, onChange:function(v){set({showContribution:v});}}),
209 el(ToggleControl,{__nextHasNoMarginBottom:true,label:'Show Margin Ratio', checked:a.showMarginRatio, onChange:function(v){set({showMarginRatio:v});}})
210 ),
211
212 el( PanelBody, { title: __( 'Typography', 'blockenberg' ), initialOpen: false },
213 el(getTypographyControl(), { label: __('Title', 'blockenberg'), value: a.typoTitle, onChange: function (v) { set({ typoTitle: v }); } }),
214 el(getTypographyControl(), { label: __('Subtitle', 'blockenberg'), value: a.typoSubtitle, onChange: function (v) { set({ typoSubtitle: v }); } }),
215 el(RangeControl,{label:'Result Font Size', value:a.resultSize, min:24,max:72,step:2, onChange:function(v){set({resultSize:v});}})
216 ),
217 el(PanelColorSettings,{title:'Colors',initialOpen:false,colorSettings:colorSettings}),
218 el(PanelBody,{title:'Sizing & Layout',initialOpen:false},
219 el(RangeControl,{label:'Card Border Radius', value:a.cardRadius, min:0,max:40,step:1, onChange:function(v){set({cardRadius:v});}}),
220 el(RangeControl,{label:'Input Border Radius',value:a.inputRadius, min:0,max:20,step:1, onChange:function(v){set({inputRadius:v});}}),
221 el(RangeControl,{label:'Max Width (px)', value:a.maxWidth, min:340,max:960,step:10,onChange:function(v){set({maxWidth:v});}}),
222 el(RangeControl,{label:'Padding Top (px)', value:a.paddingTop, min:0,max:160,step:4, onChange:function(v){set({paddingTop:v});}}),
223 el(RangeControl,{label:'Padding Bottom (px)',value:a.paddingBottom,min:0,max:160,step:4, onChange:function(v){set({paddingBottom:v});}})
224 )
225 ),
226 el('div', blockProps, el(BEPPreview, {attributes:a}))
227 );
228 },
229 save: function(props) {
230 var a = props.attributes;
231 return el('div', wp.blockEditor.useBlockProps.save(),
232 el('div', {className:'bkbg-bep-app','data-opts':JSON.stringify(a)}));
233 }
234 });
235 }() );
236