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 / frontend.js

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

269 lines 14.4 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 // ── Helpers ──────────────────────────────────────────────────────────
26 function qs(el, sel) { return el.querySelector(sel); }
27 function fmt(n, d) { return Number(n).toFixed(d !== undefined ? d : 2); }
28 function fmtCur(n, cur) { return cur + fmt(n, 2); }
29
30 function calcMarginMode(cost, margin) {
31 if (cost <= 0 || margin >= 100) return null;
32 var sp = cost / (1 - margin / 100);
33 var profit = sp - cost;
34 return { sellingPrice: sp, profit: profit, margin: margin, markup: cost > 0 ? (profit / cost) * 100 : 0 };
35 }
36 function calcMarkupMode(cost, markup) {
37 if (cost <= 0) return null;
38 var sp = cost * (1 + markup / 100);
39 var profit = sp - cost;
40 return { sellingPrice: sp, profit: profit, margin: sp > 0 ? (profit / sp) * 100 : 0, markup: markup };
41 }
42 function calcRevenueMode(revenue, cost) {
43 if (revenue <= 0) return null;
44 var profit = revenue - cost;
45 return { sellingPrice: revenue, profit: profit, margin: (profit / revenue) * 100, markup: cost > 0 ? (profit / cost) * 100 : 0 };
46 }
47
48 function scaleColor(m) {
49 if (m < 10) return '#ef4444';
50 if (m < 25) return '#f59e0b';
51 if (m < 50) return '#10b981';
52 return '#6c3fb5';
53 }
54 function scaleLabel(m) {
55 if (m < 10) return 'Low margin';
56 if (m < 25) return 'Moderate margin';
57 if (m < 50) return 'Healthy margin';
58 return 'High margin';
59 }
60
61 // ── Bootstrap ────────────────────────────────────────────────────────
62 document.querySelectorAll('.bkbg-mgc-app').forEach(function (root) {
63 var opts;
64 try { opts = JSON.parse(root.getAttribute('data-opts') || '{}'); }
65 catch (e) { return; }
66
67 var o = {
68 title: opts.title || 'Profit Margin Calculator',
69 subtitle: opts.subtitle || 'Calculate selling price, markup and gross profit instantly',
70 currency: opts.currency || '$',
71 defaultMode: opts.defaultMode || 'margin',
72 defaultCost: parseFloat(opts.defaultCost) || 50,
73 defaultMargin: parseFloat(opts.defaultMargin) || 40,
74 defaultRevenue: parseFloat(opts.defaultRevenue) || 100,
75 showMarkup: opts.showMarkup !== false,
76 showBar: opts.showBar !== false,
77 showMarginScale: opts.showMarginScale !== false,
78 accentColor: opts.accentColor || '#10b981',
79 profitColor: opts.profitColor || '#10b981',
80 costColor: opts.costColor || '#f43f5e',
81 sectionBg: opts.sectionBg || '#f0fdf4',
82 cardBg: opts.cardBg || '#ffffff',
83 inputBg: opts.inputBg || '#f9fafb',
84 resultBg: opts.resultBg || '#dcfce7',
85 titleColor: opts.titleColor || '#111827',
86 subtitleColor: opts.subtitleColor || '#6b7280',
87 labelColor: opts.labelColor || '#374151',
88 titleTypo: opts.titleTypo,
89 subtitleTypo: opts.subtitleTypo,
90 contentMaxWidth: parseInt(opts.contentMaxWidth) || 680
91 };
92
93 var mode = o.defaultMode;
94
95 // ── Build HTML ─────────────────────────────────────────────────
96 root.innerHTML = [
97 '<div class="bkbg-mgc-wrap" style="background:' + o.sectionBg + ';max-width:' + o.contentMaxWidth + 'px;--mgc-accent:' + o.accentColor + '">',
98
99 o.title ? '<h3 class="bkbg-mgc-title" style="color:' + o.titleColor + '">' + o.title + '</h3>' : '',
100 o.subtitle ? '<p class="bkbg-mgc-subtitle" style="color:' + o.subtitleColor + '">' + o.subtitle + '</p>' : '',
101
102 // tabs
103 '<div class="bkbg-mgc-tabs">',
104 '<button class="bkbg-mgc-tab" data-mode="margin">' , 'Margin %', '</button>',
105 '<button class="bkbg-mgc-tab" data-mode="markup">' , 'Markup %', '</button>',
106 '<button class="bkbg-mgc-tab" data-mode="revenue">' , 'Revenue', '</button>',
107 '</div>',
108
109 // inputs — rendered dynamically
110 '<div class="bkbg-mgc-inputs" id="mgc-inputs-grid"></div>',
111
112 // result area
113 '<div id="mgc-results"></div>',
114
115 '</div>'
116 ].join('');
117
118 var wrap = qs(root, '.bkbg-mgc-wrap');
119
120 // Apply typography CSS vars on root
121 typoCssVarsForEl(root, o.titleTypo, '--bkbg-mgc-tt-');
122 typoCssVarsForEl(root, o.subtitleTypo, '--bkbg-mgc-st-');
123
124 var inputGrid = qs(root, '#mgc-inputs-grid');
125 var resultsEl = qs(root, '#mgc-results');
126 var tabs = root.querySelectorAll('.bkbg-mgc-tab');
127
128 // ── Input state ────────────────────────────────────────────────
129 var state = {
130 cost: o.defaultCost,
131 margin: o.defaultMargin,
132 markup: o.defaultMargin, // shared default for both %
133 revenue: o.defaultRevenue
134 };
135
136 // ── Build inputs for active mode ───────────────────────────────
137 function buildInputs() {
138 var fields = [];
139 if (mode === 'margin') {
140 fields = [
141 { key:'cost', label:'Cost Price (' + o.currency + ')', val: state.cost, min:0, step:0.01 },
142 { key:'margin', label:'Target Margin (%)', val: state.margin, min:0.1, max:99.9, step:0.1 }
143 ];
144 } else if (mode === 'markup') {
145 fields = [
146 { key:'cost', label:'Cost Price (' + o.currency + ')', val: state.cost, min:0, step:0.01 },
147 { key:'markup', label:'Markup (%)', val: state.markup, min:0, step:0.1 }
148 ];
149 } else {
150 fields = [
151 { key:'revenue', label:'Revenue (' + o.currency + ')', val: state.revenue, min:0.01, step:0.01 },
152 { key:'cost', label:'Cost (' + o.currency + ')', val: state.cost, min:0, step:0.01 }
153 ];
154 }
155
156 inputGrid.innerHTML = fields.map(function (f) {
157 var maxAttr = f.max !== undefined ? ' max="' + f.max + '"' : '';
158 return '<div class="bkbg-mgc-field">' +
159 '<label style="color:' + o.labelColor + '">' + f.label + '</label>' +
160 '<input class="bkbg-mgc-input" type="number" data-key="' + f.key + '" value="' + fmt(f.val, 2) + '" min="' + (f.min||0) + '"' + maxAttr + ' step="' + (f.step||1) + '" style="background:' + o.inputBg + ';color:' + o.labelColor + '">' +
161 '</div>';
162 }).join('');
163
164 inputGrid.querySelectorAll('.bkbg-mgc-input').forEach(function (inp) {
165 inp.addEventListener('input', function () {
166 var key = inp.getAttribute('data-key');
167 var val = parseFloat(inp.value);
168 if (!isNaN(val)) { state[key] = val; }
169 renderResults();
170 });
171 });
172 }
173
174 // ── Render results ─────────────────────────────────────────────
175 function renderResults() {
176 var result;
177 if (mode === 'margin') result = calcMarginMode(state.cost, state.margin);
178 else if (mode === 'markup') result = calcMarkupMode(state.cost, state.markup);
179 else result = calcRevenueMode(state.revenue, state.cost);
180
181 if (!result) {
182 resultsEl.innerHTML = '<div class="bkbg-mgc-error">Please enter valid positive values (margin must be &lt; 100%).</div>';
183 return;
184 }
185
186 // cards
187 var cards = [
188 { label:'Selling Price', value: fmtCur(result.sellingPrice, o.currency), color: o.accentColor, bg: o.resultBg },
189 { label:'Gross Profit', value: fmtCur(result.profit, o.currency), color: o.profitColor, bg: o.resultBg },
190 { label:'Margin %', value: fmt(result.margin, 1) + '%', color: o.profitColor, bg: '#f0fdf4' }
191 ];
192 if (o.showMarkup) {
193 cards.push({ label:'Markup %', value: fmt(result.markup, 1) + '%', color: o.accentColor, bg: '#f0fdf4' });
194 }
195
196 var cardsHtml = '<div class="bkbg-mgc-cards">' +
197 cards.map(function (c) {
198 return '<div class="bkbg-mgc-card" style="background:' + c.bg + ';border-left-color:' + c.color + '">' +
199 '<div class="bkbg-mgc-card-label">' + c.label + '</div>' +
200 '<div class="bkbg-mgc-card-value" style="color:' + c.color + '">' + c.value + '</div>' +
201 '</div>';
202 }).join('') +
203 '</div>';
204
205 // bar
206 var barHtml = '';
207 if (o.showBar && result.sellingPrice > 0) {
208 var costAmt = result.sellingPrice - result.profit;
209 var costPct = Math.max(0, Math.min(100, (costAmt / result.sellingPrice) * 100));
210 var profitPct = 100 - costPct;
211 barHtml = '<div class="bkbg-mgc-bar-section">' +
212 '<div class="bkbg-mgc-bar-heading" style="color:' + o.labelColor + '">Price Breakdown</div>' +
213 '<div class="bkbg-mgc-bar-track">' +
214 '<div class="bkbg-mgc-bar-cost" style="width:' + fmt(costPct,2) + '%;background:' + o.costColor + '">' + (costPct > 15 ? fmt(costPct,0) + '% Cost' : '') + '</div>' +
215 '<div class="bkbg-mgc-bar-profit" style="width:' + fmt(profitPct,2) + '%;background:' + o.profitColor + '">' + (profitPct > 15 ? fmt(profitPct,0) + '% Profit' : '') + '</div>' +
216 '</div>' +
217 '<div class="bkbg-mgc-bar-legend">' +
218 '<span style="color:' + o.costColor + '">● Cost: ' + fmtCur(costAmt, o.currency) + '</span>' +
219 '<span style="color:' + o.profitColor + '">● Profit: ' + fmtCur(result.profit, o.currency) + '</span>' +
220 '</div>' +
221 '</div>';
222 }
223
224 // margin scale
225 var scaleHtml = '';
226 if (o.showMarginScale) {
227 var m = result.margin;
228 var sColor = scaleColor(m);
229 var sLabel = scaleLabel(m);
230 var fillPct = Math.max(0, Math.min(100, m));
231 scaleHtml = '<div class="bkbg-mgc-scale" style="background:' + o.cardBg + '">' +
232 '<div class="bkbg-mgc-scale-header">' +
233 '<span class="bkbg-mgc-scale-label" style="color:' + o.labelColor + '">Margin Health</span>' +
234 '<span class="bkbg-mgc-scale-status" style="color:' + sColor + '">' + sLabel + '</span>' +
235 '</div>' +
236 '<div class="bkbg-mgc-scale-track">' +
237 '<div class="bkbg-mgc-scale-fill" style="width:' + fmt(fillPct,2) + '%;background:' + sColor + '"></div>' +
238 '</div>' +
239 '<div class="bkbg-mgc-scale-ticks"><span>0%</span><span>25%</span><span>50%</span><span>75%</span><span>100%</span></div>' +
240 '</div>';
241 }
242
243 resultsEl.innerHTML = cardsHtml + barHtml + scaleHtml;
244 }
245
246 // ── Tab switching ──────────────────────────────────────────────
247 function activateTab(newMode) {
248 mode = newMode;
249 tabs.forEach(function (t) {
250 t.classList.toggle('active', t.getAttribute('data-mode') === mode);
251 t.style.background = t.classList.contains('active') ? o.accentColor : o.cardBg;
252 t.style.borderColor = t.classList.contains('active') ? o.accentColor : '#d1d5db';
253 t.style.color = t.classList.contains('active') ? '#ffffff' : o.labelColor;
254 });
255 buildInputs();
256 renderResults();
257 }
258
259 tabs.forEach(function (t) {
260 t.addEventListener('click', function () {
261 activateTab(t.getAttribute('data-mode'));
262 });
263 });
264
265 // ── Initial render ─────────────────────────────────────────────
266 activateTab(mode);
267 });
268 })();
269