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 / split-bill-calculator / index.js

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

198 lines 16.7 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 useEffect = wp.element.useEffect;
5 var Fragment = wp.element.Fragment;
6 var registerBlockType = wp.blocks.registerBlockType;
7 var __ = wp.i18n.__;
8 var InspectorControls = wp.blockEditor.InspectorControls;
9 var PanelColorSettings = wp.blockEditor.PanelColorSettings;
10 var useBlockProps = wp.blockEditor.useBlockProps;
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 Button = wp.components.Button;
16
17 var _tc; function getTypoControl() { return _tc || (_tc = window.bkbgTypographyControl); }
18 var _tv; function getTypoCssVars() { return _tv || (_tv = window.bkbgTypoCssVars); }
19
20 function number(value, fallback, min, max) {
21 var n = parseFloat(value);
22 return Number.isFinite(n) ? Math.min(max === undefined ? Infinity : max, Math.max(min || 0, n)) : fallback;
23 }
24
25 var labelDefaults = { bill: 'Bill Amount', people: 'Number of People', peopleHint: 'persons', tip: 'Tip Percentage', result: 'Each person pays', subtotal: 'Bill Subtotal', tipTotal: 'Tip', total: 'Total', split: 'Split {n} ways', each: 'each', validation: 'Enter a valid bill, tip (0–100%), and whole number of people (at least 1).' };
26
27 function fmt(sym, n) { return sym + n.toFixed(2); }
28
29 function SplitBillPreview(props) {
30 var a = props.attributes;
31 var labels = Object.assign({}, labelDefaults, a.labels || {});
32 var sym = a.currencySymbol || '$';
33 var accent = a.accentColor || '#6c3fb5';
34
35 var _bill = useState(String(number(a.defaultBill, 100))); var bill = _bill[0]; var setBill = _bill[1];
36 var _tip = useState(number(a.defaultTip, 15, 0, 100)); var tip = _tip[0]; var setTip = _tip[1];
37 var _people = useState(String(Math.floor(number(a.defaultPeople, 4, 1)))); var people = _people[0]; var setPeople = _people[1];
38
39 useEffect(function () { setBill(String(number(a.defaultBill, 100))); }, [a.defaultBill]);
40 useEffect(function () { setTip(number(a.defaultTip, 15, 0, 100)); }, [a.defaultTip]);
41 useEffect(function () { setPeople(String(Math.floor(number(a.defaultPeople, 4, 1)))); }, [a.defaultPeople]);
42
43 var billN = number(bill, 0);
44 var tipN = number(tip, 0, 0, 100);
45 var peopleN = Math.floor(number(people, 1, 1));
46 var valid = bill !== '' && Number.isFinite(Number(bill)) && Number(bill) >= 0 && tip !== '' && Number.isFinite(Number(tip)) && Number(tip) >= 0 && Number(tip) <= 100 && people !== '' && Number.isInteger(Number(people)) && Number(people) >= 1;
47 var tipAmt = billN * tipN / 100;
48 var total = billN + tipAmt;
49 var perPerson = a.roundUp ? Math.ceil(total / peopleN * 100) / 100 : total / peopleN;
50
51 var tipPresets = String(a.tipPresets || '10,15,20,25').split(',').map(function(s){ return number(s.trim(), 0, 0, 100); });
52
53 function inputRow(lbl, val, setVal, hint, type) {
54 return el('div', { style:{ marginBottom:'16px' }},
55 el('label', { style:{ display:'flex',justifyContent:'space-between',fontSize:'13px',fontWeight:600,color:a.labelColor,marginBottom:'5px' }},
56 el('span', null, lbl),
57 hint && el('span', { style:{ fontWeight:400,color:'#9ca3af',fontSize:'12px' }}, hint)
58 ),
59 el('input', { type: 'number', value: val, min:type === 'people' ? 1 : 0, step:type === 'people' ? 1 : 'any', 'aria-label':lbl,
60 style:{ width:'100%',padding:'11px 14px',borderRadius:number(a.inputRadius,8)+'px',border:'1.5px solid '+(a.inputBorder||'#e5e7eb'),fontSize:'16px',boxSizing:'border-box',fontFamily:'inherit' },
61 onChange: function(e){ setVal(e.target.value); }
62 })
63 );
64 }
65
66 return el('div', { className:'bkbg-sbc-app', style:{ paddingTop:a.paddingTop+'px',paddingBottom:a.paddingBottom+'px',background:a.sectionBg||undefined }},
67 el('div', { className:'bkbg-sbc-wrap', style:{ background:a.cardBg,borderRadius:number(a.cardRadius,16)+'px',maxWidth:(a.maxWidth||500)+'px',margin:'0 auto',boxShadow:'0 4px 24px rgba(0,0,0,0.09)' }},
68
69 (a.showTitle||a.showSubtitle) && el('div', { style:{marginBottom:'22px'}},
70 a.showTitle && el('h2', { className: 'bkbg-sbc-title', style:{color:a.titleColor,marginBottom:'6px'} }, a.title),
71 a.showSubtitle && el('div', { className: 'bkbg-sbc-subtitle', style:{color:a.subtitleColor} }, a.subtitle)
72 ),
73
74 inputRow(labels.bill, bill, setBill, sym),
75 inputRow(labels.people, people, setPeople, labels.peopleHint, 'people'),
76
77 // Tip %
78 el('div', { style:{marginBottom:'16px'}},
79 el('label', { style:{display:'block',fontSize:'13px',fontWeight:600,color:a.labelColor,marginBottom:'5px' }}, labels.tip),
80 a.showTipPresets && el('div', { style:{display:'flex',gap:'8px',flexWrap:'wrap',marginBottom:'10px'}},
81 tipPresets.map(function(p) {
82 var active = tipN === p;
83 return el('button', { key:p, type:'button', 'aria-pressed':tipN === p, onClick:function(){ setTip(p); }, style:{ flex:1, padding:'8px 6px', borderRadius:'8px', border:'2px solid '+(active?accent:'#e5e7eb'), background:active?(a.presetActiveBg||accent):(a.presetBg||'#f3f4f6'), color:active?(a.presetActiveColor||'#fff'):(a.presetColor||'#374151'), fontWeight:700, fontSize:'14px', cursor:'pointer', fontFamily:'inherit', minWidth:'48px' }}, p+'%');
84 }),
85 el('input', { type:'number', value:tip, min:0, max:100, step:'any', 'aria-label':labels.tip,
86 style:{width:'72px',padding:'8px 10px',borderRadius:'8px',border:'1.5px solid '+(a.inputBorder||'#e5e7eb'),fontSize:'15px',fontFamily:'inherit'},
87 onChange: function(e){ setTip(e.target.value); }
88 })
89 ),
90 !a.showTipPresets && el('input', { type:'number', value:tip, min:0, max:100, step:'any', 'aria-label':labels.tip,
91 style:{width:'100%',padding:'11px 14px',borderRadius:number(a.inputRadius,8)+'px',border:'1.5px solid '+(a.inputBorder||'#e5e7eb'),fontSize:'16px',boxSizing:'border-box',fontFamily:'inherit'},
92 onChange: function(e){ setTip(e.target.value); }
93 })
94 ),
95
96 // Per-person result
97 el('div', { 'aria-live':'polite', 'aria-atomic':true, style:{background:a.resultBg||accent,borderRadius:number(a.inputRadius,8)+'px',padding:'24px',textAlign:'center',marginBottom:a.showBreakdown?'16px':'0' }},
98 el('div', { style:{fontSize:'13px',fontWeight:600,color:a.resultColor||'#fff',marginBottom:'6px'} }, valid ? labels.result : labels.validation),
99 el('div', { className: 'bkbg-sbc-result-amount', style:{color:a.resultColor||'#fff'} }, valid ? sym + perPerson.toFixed(2) : '—')
100 ),
101
102 // Breakdown
103 a.showBreakdown && valid && el('div', { style:{background:a.breakdownBg||'#f5f3ff',border:'1.5px solid '+(a.breakdownBorder||'#ede9fe'),borderRadius:number(a.inputRadius,8)+'px',padding:'16px 20px'} },
104 [
105 [labels.subtotal, fmt(sym, billN)],
106 [labels.tipTotal + ' (' + tipN + '%)', '+' + fmt(sym, tipAmt)],
107 [labels.total, fmt(sym, total)],
108 [labels.split.replace('{n}', peopleN), fmt(sym, perPerson) + ' ' + labels.each]
109 ].map(function(row) {
110 return el('div', { key:row[0], style:{display:'flex',justifyContent:'space-between',padding:'5px 0',fontSize:'14px',color:a.labelColor,borderBottom:'1px solid '+(a.breakdownBorder||'#ede9fe')} },
111 el('span', null, row[0]),
112 el('span', { style:{fontWeight:600} }, row[1])
113 );
114 })
115 )
116 )
117 );
118 }
119
120 registerBlockType('blockenberg/split-bill-calculator', {
121 edit: function(props) {
122 var a = props.attributes; var set = props.setAttributes;
123 var blockProps = useBlockProps((function () {
124 var s = {};
125 var tv = getTypoCssVars();
126 if (tv) {
127 Object.assign(s, tv(a.titleTypo, '--bksbc-tt-'));
128 Object.assign(s, tv(a.subtitleTypo, '--bksbc-st-'));
129 Object.assign(s, tv(a.resultTypo, '--bksbc-rs-'));
130 }
131 return { style: s };
132 })());
133 var colorSettings = [
134 { value: a.accentColor, onChange: function(v){ set({accentColor:v}); }, label: 'Accent Color' },
135 { value: a.cardBg, onChange: function(v){ set({cardBg:v}); }, label: 'Card Background' },
136 { value: a.resultBg, onChange: function(v){ set({resultBg:v}); }, label: 'Result Background' },
137 { value: a.resultColor, onChange: function(v){ set({resultColor:v}); }, label: 'Result Text' },
138 { value: a.presetBg, onChange: function(v){ set({presetBg:v}); }, label: 'Tip Preset Background' },
139 { value: a.presetColor, onChange: function(v){ set({presetColor:v}); }, label: 'Tip Preset Text' },
140 { value: a.presetActiveBg, onChange: function(v){ set({presetActiveBg:v}); }, label: 'Active Preset Background' },
141 { value: a.presetActiveColor, onChange: function(v){ set({presetActiveColor:v}); }, label: 'Active Preset Text' },
142 { value: a.breakdownBg, onChange: function(v){ set({breakdownBg:v}); }, label: 'Breakdown Background' },
143 { value: a.breakdownBorder, onChange: function(v){ set({breakdownBorder:v}); }, label: 'Breakdown Border' },
144 { value: a.labelColor, onChange: function(v){ set({labelColor:v}); }, label: 'Label Color' },
145 { value: a.inputBorder, onChange: function(v){ set({inputBorder:v}); }, label: 'Input Border' },
146 { value: a.titleColor, onChange: function(v){ set({titleColor:v}); }, label: 'Title Color' },
147 { value: a.subtitleColor, onChange: function(v){ set({subtitleColor:v}); }, label: 'Subtitle Color' },
148 { value: a.sectionBg, onChange: function(v){ set({sectionBg:v}); }, label: 'Section Background' }
149 ];
150 return el(Fragment, null,
151 el(InspectorControls, null,
152 el(PanelBody, { title: 'Header', initialOpen: false },
153 el(ToggleControl, { __nextHasNoMarginBottom: true, label: 'Show Title', checked: a.showTitle, onChange: function(v){ set({showTitle:v}); } }),
154 el(ToggleControl, { __nextHasNoMarginBottom: true, label: 'Show Subtitle', checked: a.showSubtitle, onChange: function(v){ set({showSubtitle:v}); } }),
155 el(TextControl, { label: 'Title', value: a.title, onChange: function(v){ set({title:v}); } }),
156 el(TextControl, { label: 'Subtitle', value: a.subtitle, onChange: function(v){ set({subtitle:v}); } })
157 ),
158 el(PanelBody, { title: __('Interface labels', 'blockenberg'), initialOpen: false },
159 Object.keys(labelDefaults).map(function (key) {
160 return el(TextControl, { key:key, label:labelDefaults[key], value:(a.labels || {})[key] === undefined ? labelDefaults[key] : a.labels[key],
161 onChange:function (value) { var next = Object.assign({}, a.labels || {}); next[key] = value; set({labels:next}); } });
162 })
163 ),
164 el(PanelBody, { title: 'Calculator Settings', initialOpen: true },
165 el(TextControl, { label: 'Currency Symbol', value: a.currencySymbol, onChange: function(v){ set({currencySymbol:v}); } }),
166 el(TextControl, { label: 'Default Bill Amount', value: String(a.defaultBill), onChange: function(v){ set({defaultBill:parseFloat(v)||0}); } }),
167 el(TextControl, { label: 'Default Tip (%)', value: String(a.defaultTip), onChange: function(v){ set({defaultTip:parseFloat(v)||0}); } }),
168 el(TextControl, { label: 'Default People', value: String(a.defaultPeople),onChange: function(v){ set({defaultPeople:parseInt(v)||1}); } }),
169 el(ToggleControl, { __nextHasNoMarginBottom: true, label: 'Show Tip Presets', checked: a.showTipPresets, onChange: function(v){ set({showTipPresets:v}); } }),
170 a.showTipPresets && el(TextControl, { label: 'Tip Presets (comma-separated %)', value: a.tipPresets, onChange: function(v){ set({tipPresets:v}); } }),
171 el(ToggleControl, { __nextHasNoMarginBottom: true, label: 'Show Breakdown', checked: a.showBreakdown, onChange: function(v){ set({showBreakdown:v}); } }),
172 el(ToggleControl, { __nextHasNoMarginBottom: true, label: 'Round Up Per Person', checked: a.roundUp, onChange: function(v){ set({roundUp:v}); } })
173 ),
174
175 el( PanelBody, { title: __( 'Typography', 'blockenberg' ), initialOpen: false },
176 getTypoControl() ? el(getTypoControl(), { label: __('Title Typography', 'blockenberg'), value: a.titleTypo || {}, onChange: function (v) { set({ titleTypo: v }); } }) : null,
177 getTypoControl() ? el(getTypoControl(), { label: __('Subtitle Typography', 'blockenberg'), value: a.subtitleTypo || {}, onChange: function (v) { set({ subtitleTypo: v }); } }) : null,
178 getTypoControl() ? el(getTypoControl(), { label: __('Result Typography', 'blockenberg'), value: a.resultTypo || {}, onChange: function (v) { set({ resultTypo: v }); } }) : null
179 ),
180 el(PanelColorSettings, { title: 'Colors', initialOpen: false, colorSettings: colorSettings }),
181 el(PanelBody, { title: 'Sizing & Layout', initialOpen: false },
182 el(RangeControl, { label: 'Card Border Radius', value: a.cardRadius, min: 0, max: 40, step: 1, onChange: function(v){ set({cardRadius:v}); } }),
183 el(RangeControl, { label: 'Input Border Radius',value: a.inputRadius, min: 0, max: 24, step: 1, onChange: function(v){ set({inputRadius:v}); } }),
184 el(RangeControl, { label: 'Max Width (px)', value: a.maxWidth, min: 300,max: 900, step: 10, onChange: function(v){ set({maxWidth:v}); } }),
185 el(RangeControl, { label: 'Padding Top (px)', value: a.paddingTop, min: 0, max: 160, step: 4, onChange: function(v){ set({paddingTop:v}); } }),
186 el(RangeControl, { label: 'Padding Bottom (px)',value: a.paddingBottom, min: 0, max: 160, step: 4, onChange: function(v){ set({paddingBottom:v}); } })
187 )
188 ),
189 el('div', blockProps, el(SplitBillPreview, { attributes: a, setAttributes: set }))
190 );
191 },
192 save: function(props) {
193 var a = props.attributes;
194 return el('div', wp.blockEditor.useBlockProps.save(), el('div', { className: 'bkbg-sbc-app', 'data-opts': JSON.stringify({ labels: a.labels, titleTypo: a.titleTypo, subtitleTypo: a.subtitleTypo, resultTypo: a.resultTypo, title: a.title, subtitle: a.subtitle, showTitle: a.showTitle, showSubtitle: a.showSubtitle, currencySymbol: a.currencySymbol, defaultBill: a.defaultBill, defaultTip: a.defaultTip, defaultPeople: a.defaultPeople, showTipPresets: a.showTipPresets, tipPresets: a.tipPresets, showBreakdown: a.showBreakdown, roundUp: a.roundUp, accentColor: a.accentColor, cardBg: a.cardBg, resultBg: a.resultBg, resultColor: a.resultColor, presetBg: a.presetBg, presetColor: a.presetColor, presetActiveBg: a.presetActiveBg, presetActiveColor: a.presetActiveColor, breakdownBg: a.breakdownBg, breakdownBorder: a.breakdownBorder, labelColor: a.labelColor, inputBorder: a.inputBorder, titleColor: a.titleColor, subtitleColor: a.subtitleColor, sectionBg: a.sectionBg, cardRadius: a.cardRadius, inputRadius: a.inputRadius, maxWidth: a.maxWidth, paddingTop: a.paddingTop, paddingBottom: a.paddingBottom }) }));
195 }
196 });
197 }() );
198