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

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

351 lines 19.3 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 RichText = wp.blockEditor.RichText;
11 var PanelBody = wp.components.PanelBody;
12 var RangeControl = wp.components.RangeControl;
13 var TextControl = wp.components.TextControl;
14 var ToggleControl = wp.components.ToggleControl;
15 var SelectControl = wp.components.SelectControl;
16
17 var _tc, _tv;
18 function getTypoControl() { return _tc || (_tc = window.bkbgTypographyControl); }
19 function getTypoCssVars() { return _tv || (_tv = window.bkbgTypoCssVars); }
20
21 // ── Calculation helpers ──────────────────────────────────────────────
22 function calcMarginMode(cost, margin) {
23 if (cost <= 0 || margin >= 100) return null;
24 var sellingPrice = cost / (1 - margin / 100);
25 var profit = sellingPrice - cost;
26 var markup = cost > 0 ? (profit / cost) * 100 : 0;
27 return { sellingPrice: sellingPrice, profit: profit, margin: margin, markup: markup };
28 }
29 function calcMarkupMode(cost, markup) {
30 if (cost <= 0) return null;
31 var sellingPrice = cost * (1 + markup / 100);
32 var profit = sellingPrice - cost;
33 var margin = sellingPrice > 0 ? (profit / sellingPrice) * 100 : 0;
34 return { sellingPrice: sellingPrice, profit: profit, margin: margin, markup: markup };
35 }
36 function calcRevenueMode(revenue, cost) {
37 if (revenue <= 0) return null;
38 var profit = revenue - cost;
39 var margin = (profit / revenue) * 100;
40 var markup = cost > 0 ? (profit / cost) * 100 : 0;
41 return { sellingPrice: revenue, profit: profit, margin: margin, markup: markup };
42 }
43 function fmt(n, decimals) {
44 return n.toFixed(decimals !== undefined ? decimals : 2);
45 }
46
47 // ── Editor preview component ─────────────────────────────────────────
48 function EditorPreview(props) {
49 var a = props.attributes;
50 var mode = a.defaultMode;
51 var cost = parseFloat(a.defaultCost) || 0;
52 var margin = parseFloat(a.defaultMargin) || 0;
53 var rev = parseFloat(a.defaultRevenue) || 0;
54 var result = mode === 'margin' ? calcMarginMode(cost, margin)
55 : mode === 'markup' ? calcMarkupMode(cost, margin)
56 : calcRevenueMode(rev, cost);
57
58 var wrapStyle = {
59 background: a.sectionBg,
60 borderRadius: '12px',
61 padding: '32px 24px',
62 maxWidth: a.contentMaxWidth + 'px',
63 margin: '0 auto',
64 fontFamily: 'inherit',
65 boxSizing: 'border-box'
66 };
67 var tabsRow = el('div', { style: { display:'flex', gap:'8px', marginBottom:'20px' } },
68 ['margin','markup','revenue'].map(function(m) {
69 var active = m === mode;
70 return el('button', {
71 key: m,
72 style: {
73 padding: '8px 18px',
74 borderRadius: '8px',
75 border: '2px solid ' + (active ? a.accentColor : '#d1d5db'),
76 background: active ? a.accentColor : a.cardBg,
77 color: active ? '#fff' : a.labelColor,
78 fontWeight: active ? '700' : '400',
79 cursor: 'pointer',
80 fontSize: '14px',
81 textTransform:'capitalize'
82 }
83 }, m === 'margin' ? 'Margin %' : m === 'markup' ? 'Markup %' : 'Revenue');
84 })
85 );
86
87 var inputStyle = {
88 background: a.inputBg,
89 border: '1.5px solid #e5e7eb',
90 borderRadius: '8px',
91 padding: '10px 14px',
92 fontSize: '15px',
93 width: '100%',
94 boxSizing: 'border-box',
95 color: a.labelColor
96 };
97 var labelStyle = { fontSize:'13px', fontWeight:'600', color:a.labelColor, marginBottom:'4px', display:'block' };
98
99 var inputsRow = el('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:'16px', marginBottom:'24px' } },
100 el('div', null,
101 el('span', { style: labelStyle }, mode === 'revenue' ? 'Revenue (' + a.currency + ')' : 'Cost Price (' + a.currency + ')'),
102 el('input', { style: inputStyle, readOnly: true, value: mode === 'revenue' ? fmt(rev, 2) : fmt(cost, 2) })
103 ),
104 el('div', null,
105 el('span', { style: labelStyle },
106 mode === 'margin' ? 'Target Margin (%)' :
107 mode === 'markup' ? 'Markup (%)' :
108 'Cost Price (' + a.currency + ')'
109 ),
110 el('input', { style: inputStyle, readOnly: true,
111 value: mode === 'margin' ? fmt(margin, 1) :
112 mode === 'markup' ? fmt(margin, 1) :
113 fmt(cost, 2)
114 })
115 )
116 );
117
118 var cards = result ? [
119 { label:'Selling Price', value: a.currency + fmt(result.sellingPrice, 2), bg: a.resultBg, color: a.accentColor },
120 { label:'Gross Profit', value: a.currency + fmt(result.profit, 2), bg: a.resultBg, color: a.profitColor },
121 { label:'Margin %', value: fmt(result.margin, 1) + '%', bg: '#f0fdf4', color: a.profitColor },
122 { label:'Markup %', value: fmt(result.markup, 1) + '%', bg: '#f0fdf4', color: a.accentColor }
123 ] : [];
124
125 var cardsRow = el('div', { style:{ display:'grid', gridTemplateColumns:'repeat(2, 1fr)', gap:'12px', marginBottom:'20px' } },
126 cards.map(function(c) {
127 return el('div', {
128 key: c.label,
129 style: { background: c.bg, borderRadius:'10px', padding:'14px 16px', borderLeft:'4px solid '+c.color }
130 },
131 el('div', { style:{ fontSize:'11px', fontWeight:'600', color:'#6b7280', textTransform:'uppercase', letterSpacing:'0.05em', marginBottom:'6px' } }, c.label),
132 el('div', { style:{ fontSize:'22px', fontWeight:'700', color: c.color } }, c.value)
133 );
134 })
135 );
136
137 var barSection = null;
138 if (a.showBar && result && result.sellingPrice > 0) {
139 var costPct = Math.max(0, Math.min(100, (result.sellingPrice - result.profit) / result.sellingPrice * 100));
140 var profitPct = 100 - costPct;
141 barSection = el('div', { style:{ marginBottom:'20px' } },
142 el('div', { style:{ fontSize:'12px', fontWeight:'600', color:a.labelColor, marginBottom:'6px' } }, 'Price Breakdown'),
143 el('div', { style:{ display:'flex', borderRadius:'8px', overflow:'hidden', height:'28px' } },
144 el('div', { style:{ width: costPct+'%', background: a.costColor, display:'flex', alignItems:'center', justifyContent:'center', color:'#fff', fontSize:'11px', fontWeight:'700' } },
145 costPct > 15 ? fmt(costPct,0)+'% Cost' : ''
146 ),
147 el('div', { style:{ width: profitPct+'%', background: a.profitColor, display:'flex', alignItems:'center', justifyContent:'center', color:'#fff', fontSize:'11px', fontWeight:'700' } },
148 profitPct > 15 ? fmt(profitPct,0)+'% Profit' : ''
149 )
150 ),
151 el('div', { style:{ display:'flex', gap:'16px', marginTop:'6px' } },
152 el('span', { style:{ fontSize:'12px', color: a.costColor, fontWeight:'600' } }, '● Cost: ' + a.currency + fmt(result.sellingPrice - result.profit, 2)),
153 el('span', { style:{ fontSize:'12px', color: a.profitColor, fontWeight:'600' } }, '● Profit: ' + a.currency + fmt(result.profit, 2))
154 )
155 );
156 }
157
158 var scaleSection = null;
159 if (a.showMarginScale && result) {
160 var m = result.margin;
161 var scaleColor = m < 10 ? '#ef4444' : m < 25 ? '#f59e0b' : m < 50 ? '#10b981' : '#6c3fb5';
162 var scaleLabel = m < 10 ? 'Low margin' : m < 25 ? 'Moderate margin' : m < 50 ? 'Healthy margin' : 'High margin';
163 scaleSection = el('div', { style:{ background: a.cardBg, borderRadius:'10px', padding:'14px 16px', border:'1.5px solid #e5e7eb' } },
164 el('div', { style:{ display:'flex', justifyContent:'space-between', marginBottom:'8px' } },
165 el('span', { style:{ fontSize:'13px', fontWeight:'600', color: a.labelColor } }, 'Margin Health'),
166 el('span', { style:{ fontSize:'13px', fontWeight:'700', color: scaleColor } }, scaleLabel)
167 ),
168 el('div', { style:{ background:'#e5e7eb', borderRadius:'999px', height:'8px', overflow:'hidden' } },
169 el('div', { style:{ width: Math.min(m, 100)+'%', background: scaleColor, height:'100%', borderRadius:'999px', transition:'width 0.4s' } })
170 ),
171 el('div', { style:{ display:'flex', justifyContent:'space-between', marginTop:'4px' } },
172 el('span', { style:{ fontSize:'11px', color:'#9ca3af' } }, '0%'),
173 el('span', { style:{ fontSize:'11px', color:'#9ca3af' } }, '50%'),
174 el('span', { style:{ fontSize:'11px', color:'#9ca3af' } }, '100%')
175 )
176 );
177 }
178
179 return el('div', { style: wrapStyle },
180 el(RichText, { tagName:'h3', value: a.title, onChange: function(v){ props.setAttributes({ title: v }); },
181 className: 'bkbg-mgc-title',
182 style:{ color: a.titleColor, margin:'0 0 6px 0' },
183 placeholder: 'Block title...'
184 }),
185 el(RichText, { tagName:'p', value: a.subtitle, onChange: function(v){ props.setAttributes({ subtitle: v }); },
186 className: 'bkbg-mgc-subtitle',
187 style:{ color: a.subtitleColor, margin:'0 0 24px 0' },
188 placeholder: 'Subtitle...'
189 }),
190 tabsRow,
191 inputsRow,
192 cardsRow,
193 barSection,
194 scaleSection
195 );
196 }
197
198 // ── Register block ───────────────────────────────────────────────────
199 registerBlockType('blockenberg/margin-calculator', {
200 edit: function(props) {
201 var a = props.attributes;
202 var setAttributes = props.setAttributes;
203
204 var colorControls = [
205 { value: a.accentColor, onChange: function(v){ setAttributes({ accentColor: v||'#10b981' }); }, label: 'Accent color' },
206 { value: a.profitColor, onChange: function(v){ setAttributes({ profitColor: v||'#10b981' }); }, label: 'Profit color' },
207 { value: a.costColor, onChange: function(v){ setAttributes({ costColor: v||'#f43f5e' }); }, label: 'Cost color' },
208 { value: a.sectionBg, onChange: function(v){ setAttributes({ sectionBg: v||'#f0fdf4' }); }, label: 'Section background' },
209 { value: a.cardBg, onChange: function(v){ setAttributes({ cardBg: v||'#ffffff' }); }, label: 'Card background' },
210 { value: a.inputBg, onChange: function(v){ setAttributes({ inputBg: v||'#f9fafb' }); }, label: 'Input background' },
211 { value: a.resultBg, onChange: function(v){ setAttributes({ resultBg: v||'#dcfce7' }); }, label: 'Result background' },
212 { value: a.titleColor, onChange: function(v){ setAttributes({ titleColor: v||'#111827' }); }, label: 'Title color' },
213 { value: a.subtitleColor, onChange: function(v){ setAttributes({ subtitleColor: v||'#6b7280' }); }, label: 'Subtitle color' },
214 { value: a.labelColor, onChange: function(v){ setAttributes({ labelColor: v||'#374151' }); }, label: 'Label color' }
215 ];
216
217 return el(Fragment, null,
218 el(InspectorControls, null,
219 el(PanelBody, { title: 'Calculator Settings', initialOpen: true },
220 el(SelectControl, {
221 label: 'Default Mode',
222 value: a.defaultMode,
223 options: [
224 { label: 'Margin % (Cost → Price)', value: 'margin' },
225 { label: 'Markup % (Cost → Price)', value: 'markup' },
226 { label: 'Revenue (Revenue + Cost)', value: 'revenue' }
227 ],
228 onChange: function(v){ setAttributes({ defaultMode: v }); }
229 }),
230 el(TextControl, {
231 label: 'Currency Symbol',
232 value: a.currency,
233 onChange: function(v){ setAttributes({ currency: v }); }
234 }),
235 el(TextControl, {
236 label: 'Default Cost ($)',
237 type: 'number',
238 value: String(a.defaultCost),
239 onChange: function(v){ setAttributes({ defaultCost: parseFloat(v)||0 }); }
240 }),
241 el(TextControl, {
242 label: 'Default Margin / Markup (%)',
243 type: 'number',
244 value: String(a.defaultMargin),
245 onChange: function(v){ setAttributes({ defaultMargin: parseFloat(v)||0 }); }
246 }),
247 el(TextControl, {
248 label: 'Default Revenue ($)',
249 type: 'number',
250 value: String(a.defaultRevenue),
251 onChange: function(v){ setAttributes({ defaultRevenue: parseFloat(v)||0 }); }
252 })
253 ),
254 el(PanelBody, { title: 'Display Options', initialOpen: false },
255 el(ToggleControl, {
256 __nextHasNoMarginBottom: true,
257 label: 'Show Breakdown Bar',
258 checked: a.showBar,
259 onChange: function(v){ setAttributes({ showBar: v }); }
260 }),
261 el(ToggleControl, {
262 __nextHasNoMarginBottom: true,
263 label: 'Show Markup %',
264 checked: a.showMarkup,
265 onChange: function(v){ setAttributes({ showMarkup: v }); }
266 }),
267 el(ToggleControl, {
268 __nextHasNoMarginBottom: true,
269 label: 'Show Margin Health Scale',
270 checked: a.showMarginScale,
271 onChange: function(v){ setAttributes({ showMarginScale: v }); }
272 })
273 ),
274 el(PanelBody, { title: 'Sizing', initialOpen: false },
275 el(RangeControl, {
276 label: 'Max Width (px)',
277 value: a.contentMaxWidth,
278 min: 400, max: 1200, step: 20,
279 onChange: function(v){ setAttributes({ contentMaxWidth: v }); }
280 })
281 ),
282
283 el( PanelBody, { title: __( 'Typography', 'blockenberg' ), initialOpen: false },
284 (function () { var C = getTypoControl(); return C ? el(C, { label: 'Title', value: a.titleTypo, onChange: function (v) { setAttributes({ titleTypo: v }); } }) : null; })(),
285 (function () { var C = getTypoControl(); return C ? el(C, { label: 'Subtitle', value: a.subtitleTypo, onChange: function (v) { setAttributes({ subtitleTypo: v }); } }) : null; })()
286 ),
287 el(PanelColorSettings, {
288 title: 'Colors',
289 initialOpen: false,
290 disableCustomGradients: true,
291 colorSettings: colorControls
292 })
293 ),
294 el('div', useBlockProps((function () {
295 var _tvFn = getTypoCssVars();
296 var s = {};
297 if (_tvFn) {
298 Object.assign(s, _tvFn(a.titleTypo, '--bkbg-mgc-tt-'));
299 Object.assign(s, _tvFn(a.subtitleTypo, '--bkbg-mgc-st-'));
300 }
301 return { style: s };
302 })()),
303 el(EditorPreview, { attributes: a, setAttributes: setAttributes })
304 )
305 );
306 },
307
308 save: function(props) {
309 var a = props.attributes;
310 var _tvFn = getTypoCssVars();
311 return el('div', useBlockProps.save((function () {
312 var s = {};
313 if (_tvFn) {
314 Object.assign(s, _tvFn(a.titleTypo, '--bkbg-mgc-tt-'));
315 Object.assign(s, _tvFn(a.subtitleTypo, '--bkbg-mgc-st-'));
316 }
317 return { style: s };
318 })()),
319 el('div', {
320 className: 'bkbg-mgc-app',
321 'data-opts': JSON.stringify({
322 title: a.title,
323 subtitle: a.subtitle,
324 currency: a.currency,
325 defaultMode: a.defaultMode,
326 defaultCost: a.defaultCost,
327 defaultMargin: a.defaultMargin,
328 defaultRevenue: a.defaultRevenue,
329 showMarkup: a.showMarkup,
330 showBar: a.showBar,
331 showMarginScale: a.showMarginScale,
332 accentColor: a.accentColor,
333 profitColor: a.profitColor,
334 costColor: a.costColor,
335 sectionBg: a.sectionBg,
336 cardBg: a.cardBg,
337 inputBg: a.inputBg,
338 resultBg: a.resultBg,
339 titleColor: a.titleColor,
340 subtitleColor: a.subtitleColor,
341 labelColor: a.labelColor,
342 titleTypo: a.titleTypo,
343 subtitleTypo: a.subtitleTypo,
344 contentMaxWidth: a.contentMaxWidth
345 })
346 })
347 );
348 }
349 });
350 }() );
351