PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / 2.2.1
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More v2.2.1
2.4.1 2.4.0 2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.2 2.2.1 2.2.0 2.1.2 2.1.1 trunk 0.0.1 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 All 68 releases
better-payment / src / campaign-builder / components / ElementCard.js

ElementCard.js in Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More 2.2.1, at src/campaign-builder/components/ElementCard.js

788 lines 37.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { useState, useEffect, useCallback } from '@wordpress/element';
2 import apiFetch from '@wordpress/api-fetch';
3 import { __ } from '@wordpress/i18n';
4
5 const CUSTOM_ICONS = {
6 progress_bar: (
7 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
8 <rect x="2" y="10" width="20" height="4" rx="2" fill="currentColor" fillOpacity="0.22"/>
9 <rect x="2" y="10" width="12" height="4" rx="2" fill="currentColor"/>
10 <circle cx="14" cy="12" r="3" fill="currentColor"/>
11 </svg>
12 ),
13 donation_form: (
14 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
15 <path d="M12 9c-.5-1.3-1.8-2.2-3.2-2.2C6.9 6.8 5.4 8.3 5.4 10.1c0 2.3 2.3 4.4 6.6 7.1 4.3-2.7 6.6-4.8 6.6-7.1 0-1.8-1.5-3.3-3.4-3.3C13.8 6.8 12.5 7.7 12 9z" fill="currentColor"/>
16 <rect x="6" y="18.5" width="12" height="3" rx="1.5" stroke="currentColor" strokeWidth="1.4" fill="none"/>
17 <line x1="9.5" y1="18.5" x2="9.5" y2="17" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round"/>
18 <line x1="12" y1="18.5" x2="12" y2="16.5" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round"/>
19 <line x1="14.5" y1="18.5" x2="14.5" y2="17" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round"/>
20 </svg>
21 ),
22 };
23 import { useSortable } from '@dnd-kit/sortable';
24 import { CSS } from '@dnd-kit/utilities';
25 import { getElement } from '../fields/FieldRegistry';
26 import { openWpMedia } from '../utils/media';
27 import { getGlobalCurrencySymbol } from '../utils/currency';
28
29 // Mirror of RendererService::scale_inline_font_sizes() — bumps every explicit
30 // px font-size by +2px so editor preview matches the 16px-base frontend.
31 function scaleInlineFontSizes( html ) {
32 if ( ! html ) return html;
33 return html.replace( /\bfont-size\s*:\s*(\d+(?:\.\d+)?)px/gi, ( _, size ) => {
34 return `font-size: ${ parseFloat( size ) + 2 }px`;
35 } );
36 }
37
38 /**
39 * A single element card on the canvas — sortable via @dnd-kit/sortable.
40 * Elements that have a live preview render their content directly;
41 * all others fall back to the generic icon + label row.
42 */
43 export default function ElementCard( { element, columnId, index, totalElements, meta, campaignId, isSelected, dispatch, isDropTarget } ) {
44 const def = getElement( element.type );
45
46 const {
47 attributes,
48 listeners,
49 setNodeRef,
50 transform,
51 transition,
52 isDragging,
53 } = useSortable( {
54 id: element.id,
55 data: { source: 'canvas', columnId, element },
56 } );
57
58 const style = {
59 transform: CSS.Transform.toString( transform ),
60 transition,
61 opacity: isDragging ? 0.4 : 1,
62 };
63
64 const handleClick = ( e ) => {
65 e.stopPropagation();
66 if ( isSelected ) {
67 dispatch( { type: 'DESELECT_ELEMENT' } );
68 } else {
69 dispatch( { type: 'SELECT_ELEMENT', elementId: element.id, columnId } );
70 }
71 };
72
73 const handleEdit = ( e ) => {
74 e.stopPropagation();
75 dispatch( { type: 'SELECT_ELEMENT', elementId: element.id, columnId } );
76 };
77
78 const handleDelete = ( e ) => {
79 e.stopPropagation();
80 dispatch( { type: 'REMOVE_ELEMENT', columnId, elementId: element.id } );
81 };
82
83 const handleMoveUp = ( e ) => {
84 e.stopPropagation();
85 dispatch( { type: 'REORDER_ELEMENTS', columnId, oldIndex: index, newIndex: index - 1 } );
86 };
87
88 const handleMoveDown = ( e ) => {
89 e.stopPropagation();
90 dispatch( { type: 'REORDER_ELEMENTS', columnId, oldIndex: index, newIndex: index + 1 } );
91 };
92
93 const isFirst = index === 0;
94 const isLast = index === totalElements - 1;
95
96 const preview = getElementPreview( element, meta, dispatch, campaignId, columnId );
97
98 return (
99 <div
100 ref={ setNodeRef }
101 style={ style }
102 className={ `bp-element-card${ isSelected ? ' is-selected' : '' }${ preview ? ' bp-element-card--preview' : '' }${ isDropTarget ? ' is-drop-target' : '' }` }
103 onClick={ handleClick }
104 { ...attributes }
105 >
106 { /* Floating hover toolbar */ }
107 <div className="bp-element-card__toolbar">
108 <button
109 type="button"
110 className="bp-element-card__toolbar-btn bp-element-card__toolbar-btn--drag"
111 title={ __( 'Drag to reorder', 'better-payment' ) }
112 { ...listeners }
113 >
114 <svg width="14" height="14" viewBox="0 0 16 16" fill="none">
115 <path d="M6 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm4 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-4 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm4 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-4 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm4 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0z" fill="currentColor"/>
116 </svg>
117 </button>
118
119 { ! isFirst && (
120 <button
121 type="button"
122 className="bp-element-card__toolbar-btn"
123 title={ __( 'Move up', 'better-payment' ) }
124 onClick={ handleMoveUp }
125 >
126 <svg width="14" height="14" viewBox="0 0 16 16" fill="none">
127 <path d="M8 12V4M4 8l4-4 4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
128 </svg>
129 </button>
130 ) }
131
132 { ! isLast && (
133 <button
134 type="button"
135 className="bp-element-card__toolbar-btn"
136 title={ __( 'Move down', 'better-payment' ) }
137 onClick={ handleMoveDown }
138 >
139 <svg width="14" height="14" viewBox="0 0 16 16" fill="none">
140 <path d="M8 4v8m4-4-4 4-4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
141 </svg>
142 </button>
143 ) }
144
145 <button
146 type="button"
147 className="bp-element-card__toolbar-btn"
148 title={ __( 'Edit element', 'better-payment' ) }
149 onClick={ handleEdit }
150 >
151 <svg width="14" height="14" viewBox="0 0 16 16" fill="none">
152 <path d="M11.5 2.5l2 2-7 7H4.5v-2l7-7z" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round"/>
153 </svg>
154 </button>
155
156 <button
157 type="button"
158 className="bp-element-card__toolbar-btn bp-element-card__toolbar-btn--delete"
159 title={ __( 'Delete', 'better-payment' ) }
160 onClick={ handleDelete }
161 >
162 <svg width="14" height="14" viewBox="0 0 16 16" fill="none">
163 <path d="M2 4h12M5.333 4V2.667C5.333 2.313 5.473 1.974 5.724 1.724 5.974 1.473 6.313 1.333 6.667 1.333H9.333C9.687 1.333 10.026 1.473 10.276 1.724 10.527 1.974 10.667 2.313 10.667 2.667V4M12.667 4 12 13.333C12 13.687 11.86 14.026 11.609 14.276 11.359 14.527 11.02 14.667 10.667 14.667H5.333C4.98 14.667 4.641 14.527 4.391 14.276 4.14 14.026 4 13.687 4 13.333L3.333 4" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round"/>
164 </svg>
165 </button>
166 </div>
167
168 { /* Non-preview fallback body */ }
169 { ! preview && (
170 <div className="bp-element-card__body">
171 { CUSTOM_ICONS[ element.type ]
172 ? <span className="bp-cb-field-card__svg-icon">{ CUSTOM_ICONS[ element.type ] }</span>
173 : <span className={ `dashicons dashicons-${ def?.icon || 'block-default' }` } />
174 }
175 <span className="bp-element-card__label">{ def?.label || element.type }</span>
176 </div>
177 ) }
178
179 { /* Live preview body */ }
180 { preview && (
181 <div className="bp-element-card__preview">
182 { preview }
183 </div>
184 ) }
185 </div>
186 );
187 }
188
189 // ── Per-element live previews ─────────────────────────────────────────────────
190
191 function getElementPreview( element, meta, dispatch, campaignId, columnId ) {
192 switch ( element.type ) {
193 case 'campaign_title':
194 return <CampaignTitlePreview element={ element } meta={ meta } />;
195 case 'campaign_description':
196 return <CampaignDescriptionPreview element={ element } />;
197 case 'photo':
198 return <PhotoPreview element={ element } columnId={ columnId } dispatch={ dispatch } />;
199 case 'social_links':
200 return <SocialLinksPreview element={ element } />;
201 case 'social_sharing':
202 return <SocialSharingPreview element={ element } />;
203 case 'progress_bar':
204 return <ProgressBarPreview element={ element } meta={ meta } campaignId={ campaignId } />;
205 case 'donate_amount':
206 return <DonateAmountPreview element={ element } meta={ meta } dispatch={ dispatch } />;
207 case 'donation_form':
208 return <DonationFormPreview element={ element } meta={ meta } />;
209 case 'campaign_summary':
210 return <CampaignSummaryPreview element={ element } meta={ meta } campaignId={ campaignId } />;
211 case 'organizer':
212 return <OrganizerPreview element={ element } />;
213 default:
214 return null;
215 }
216 }
217
218 // ── Campaign Summary preview ──────────────────────────────────────────────────
219
220 function CampaignSummaryPreview( { element, meta, campaignId } ) {
221 const s = element.settings || {};
222 const headline = s.headline || '';
223 const showRaised = s.show_raised !== false;
224 const showDonors = s.show_donors !== false;
225 const showPercent = s.show_percent !== false;
226 const showDays = s.show_days !== false;
227
228 const [ stats, setStats ] = useState( null );
229
230 useEffect( () => {
231 if ( ! campaignId ) return;
232 apiFetch( { path: `better-payment/v1/campaigns/${ campaignId }/stats` } )
233 .then( ( data ) => setStats( data ) )
234 .catch( () => {} );
235 }, [ campaignId ] );
236
237 const raised = stats ? parseFloat( stats.total_raised ) : 0;
238 const donors = stats ? stats.donor_count : 0;
239 const goal = parseFloat( meta?.bpc_goal_amount ) || 0;
240 const days = meta?.bpc_end_date
241 ? Math.max( 0, Math.ceil( ( new Date( meta.bpc_end_date ) - new Date() ) / 86400000 ) )
242 : 0;
243 const percent = goal > 0 ? Math.min( 100, Math.round( ( raised / goal ) * 1000 ) / 10 ) : 0;
244
245 const currencySymbol = getGlobalCurrencySymbol();
246
247 const raisedFormatted = `${ currencySymbol }${ Number( raised ).toLocaleString( 'en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 } ) }`;
248
249 return (
250 <div className="bp-element-preview__summary">
251 { headline && (
252 <p className="bp-element-preview__summary-headline">{ headline }</p>
253 ) }
254 <div className="bp-element-preview__summary-grid">
255 { showRaised && (
256 <div className="bp-element-preview__summary-item">
257 <strong>{ raisedFormatted }</strong>
258 <span>{ __( 'Raised', 'better-payment' ) }</span>
259 </div>
260 ) }
261 { showDonors && (
262 <div className="bp-element-preview__summary-item">
263 <strong>{ donors }</strong>
264 <span>{ __( 'Donors', 'better-payment' ) }</span>
265 </div>
266 ) }
267 { showPercent && (
268 <div className="bp-element-preview__summary-item">
269 <strong>{ percent }%</strong>
270 <span>{ __( 'Funded', 'better-payment' ) }</span>
271 </div>
272 ) }
273 { showDays && (
274 <div className="bp-element-preview__summary-item">
275 <strong>{ days }</strong>
276 <span>{ __( 'Days Left', 'better-payment' ) }</span>
277 </div>
278 ) }
279 </div>
280 </div>
281 );
282 }
283
284 // ── Campaign Description preview ─────────────────────────────────────────────
285
286 function CampaignDescriptionPreview( { element } ) {
287 const s = element.settings || {};
288 const headline = s.headline || '';
289 const content = s.content || '';
290 const width = s.width ?? 100;
291 const align = s.align || 'left';
292
293 const wrapStyle = {
294 width: `${ width }%`,
295 textAlign: align,
296 margin: align === 'center' ? '0 auto' : align === 'right' ? '0 0 0 auto' : '0',
297 };
298
299 if ( ! headline && ! content ) {
300 return (
301 <div className="bp-element-preview__description-empty">
302 <span className="dashicons dashicons-editor-paragraph" />
303 <span>{ __( 'Campaign Description', 'better-payment' ) }</span>
304 </div>
305 );
306 }
307
308 return (
309 <div className="bp-element-preview__description" style={ wrapStyle }>
310 { headline && (
311 <p className="bp-element-preview__description-headline">{ headline }</p>
312 ) }
313 { content && (
314 <div
315 className="bp-element-preview__description-content"
316 dangerouslySetInnerHTML={ { __html: scaleInlineFontSizes( content ) } }
317 />
318 ) }
319 </div>
320 );
321 }
322
323 // ── Progress Bar preview ──────────────────────────────────────────────────────
324
325 function ProgressBarPreview( { element, meta, campaignId } ) {
326 const s = element.settings || {};
327 const headline = s.headline || '';
328 const showDonated = s.show_donated !== false;
329 const showGoal = s.show_goal !== false;
330 const donateLabel = s.donate_label || 'Donated:';
331 const goalLabel = s.goal_label || 'Goal:';
332 const width = s.width ?? 100;
333 const align = s.align || 'left';
334 const primary = meta?.bpc_color_primary || '#6b63f6';
335
336 const [ stats, setStats ] = useState( null );
337
338 useEffect( () => {
339 if ( ! campaignId ) return;
340 apiFetch( { path: `better-payment/v1/campaigns/${ campaignId }/stats` } )
341 .then( ( data ) => setStats( data ) )
342 .catch( () => {} );
343 }, [ campaignId ] );
344
345 const raised = stats ? stats.total_raised : 0;
346 const goal = parseFloat( meta?.bpc_goal_amount ) || 0;
347 const progress = goal > 0 ? Math.min( 100, ( raised / goal ) * 100 ) : 0;
348
349 const wrapStyle = {
350 width: `${ width }%`,
351 margin: align === 'center' ? '0 auto' : align === 'right' ? '0 0 0 auto' : '0',
352 };
353
354 const roundAmounts = s.round_amounts || false;
355 const currencySymbol = getGlobalCurrencySymbol();
356
357 // Display progress: ceiling integer when round_amounts, else 1 decimal from API.
358 const rawRatio = goal > 0 ? Math.min( 100, ( raised / goal ) * 100 ) : 0;
359 const displayProgress = roundAmounts
360 ? Math.ceil( rawRatio )
361 : progress; // 1-decimal float from CampaignStats
362
363 const goalFormatted = currencySymbol + ( roundAmounts ? Math.ceil( goal ).toLocaleString() : Number( goal ).toFixed( 2 ) );
364
365 return (
366 <div className="bp-element-preview__progress" style={ wrapStyle }>
367 { headline && (
368 <p className="bp-element-preview__progress-headline">{ headline }</p>
369 ) }
370 <div className="bp-element-preview__progress-track">
371 <div
372 className="bp-element-preview__progress-fill"
373 style={ { width: `${ progress }%`, backgroundColor: primary } }
374 />
375 </div>
376 { ( showDonated || showGoal ) && (
377 <div className="bp-element-preview__progress-labels">
378 { showDonated && (
379 <span className="bp-element-preview__progress-donated">
380 { donateLabel } { displayProgress }%
381 </span>
382 ) }
383 { showGoal && (
384 <span className="bp-element-preview__progress-goal">
385 { goalLabel } { goalFormatted }
386 </span>
387 ) }
388 </div>
389 ) }
390 </div>
391 );
392 }
393
394 // ── Photo preview ─────────────────────────────────────────────────────────────
395
396 function PhotoPreview( { element, columnId, dispatch } ) {
397 const s = element.settings || {};
398
399 const handleImageClick = useCallback( ( e ) => {
400 e.stopPropagation();
401 openWpMedia( ( { url, id, sizes } ) => {
402 dispatch( {
403 type: 'UPDATE_ELEMENT_SETTINGS',
404 elementId: element.id,
405 settings: { src: url, src_id: id, src_sizes: sizes },
406 } );
407 } );
408 }, [ element.id, dispatch ] );
409 const src = s.src || '';
410 const alt = s.alt || '';
411 const width = s.width ?? 100;
412 const align = s.align || 'center';
413 const size = s.size || 'full';
414 const sizes = s.src_sizes || {};
415 const srcId = s.src_id || 0;
416
417 // Keyed by attachment ID — fetched once per ID, cached for all size changes.
418 const [ fetchedSizes, setFetchedSizes ] = useState( {} );
419
420 useEffect( () => {
421 if ( ! srcId ) {
422 setFetchedSizes( {} );
423 return;
424 }
425 let cancelled = false;
426 apiFetch( { path: `/wp/v2/media/${ srcId }` } )
427 .then( ( data ) => {
428 if ( cancelled ) return;
429 const raw = data?.media_details?.sizes || {};
430 const mapped = {};
431 Object.keys( raw ).forEach( ( k ) => {
432 mapped[ k ] = raw[ k ].source_url;
433 } );
434 setFetchedSizes( mapped );
435 } )
436 .catch( () => {} );
437 return () => { cancelled = true; };
438 }, [ srcId ] );
439
440 // Priority: src_sizes (from media modal) → fetched from REST API → original src.
441 const resolvedUrl = size !== 'full'
442 ? ( sizes[ size ]?.url || fetchedSizes[ size ] || src )
443 : src;
444
445 const wrapStyle = { textAlign: align };
446 const imgStyle = {
447 width: 'auto',
448 maxWidth: `${ width }%`,
449 height: 'auto',
450 display: 'block',
451 borderRadius: '4px',
452 margin: align === 'center' ? '0 auto' : align === 'right' ? '0 0 0 auto' : '0',
453 };
454
455 if ( ! src ) {
456 return (
457 <div className="bp-element-preview__photo-empty" onClick={ handleImageClick } style={ { cursor: 'pointer' } }>
458 <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="#bbb">
459 <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
460 </svg>
461 </div>
462 );
463 }
464
465 return (
466 <div className="bp-element-preview__photo" style={ wrapStyle }>
467 <img
468 src={ resolvedUrl }
469 alt={ alt }
470 style={ { ...imgStyle, cursor: 'pointer' } }
471 onClick={ handleImageClick }
472 />
473 </div>
474 );
475 }
476
477 // ── Social Links preview ──────────────────────────────────────────────────────
478
479 const SOCIAL_ICONS = {
480 twitter: (
481 <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
482 <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.744l7.73-8.835L1.254 2.25H8.08l4.253 5.622zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
483 </svg>
484 ),
485 facebook: (
486 <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
487 <path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
488 </svg>
489 ),
490 linkedin: (
491 <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
492 <path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
493 </svg>
494 ),
495 instagram: (
496 <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
497 <path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162S8.597 18.163 12 18.163s6.162-2.759 6.162-6.162S15.403 5.838 12 5.838zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/>
498 </svg>
499 ),
500 tiktok: (
501 <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
502 <path d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z"/>
503 </svg>
504 ),
505 pinterest: (
506 <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
507 <path d="M12 0C5.373 0 0 5.372 0 12c0 5.084 3.163 9.426 7.627 11.174-.105-.949-.2-2.405.042-3.441.218-.937 1.407-5.965 1.407-5.965s-.359-.719-.359-1.782c0-1.668.967-2.914 2.171-2.914 1.023 0 1.518.769 1.518 1.69 0 1.029-.655 2.568-.994 3.995-.283 1.194.599 2.169 1.777 2.169 2.133 0 3.772-2.249 3.772-5.495 0-2.873-2.064-4.882-5.012-4.882-3.414 0-5.418 2.561-5.418 5.207 0 1.031.397 2.138.893 2.738a.36.36 0 01.083.345l-.333 1.36c-.053.22-.174.267-.402.161-1.499-.698-2.436-2.889-2.436-4.649 0-3.785 2.75-7.262 7.929-7.262 4.163 0 7.398 2.967 7.398 6.931 0 4.136-2.607 7.464-6.227 7.464-1.216 0-2.359-.632-2.75-1.378l-.748 2.853c-.271 1.043-1.002 2.35-1.492 3.146C9.57 23.812 10.763 24 12 24c6.627 0 12-5.373 12-12S18.627 0 12 0z"/>
508 </svg>
509 ),
510 youtube: (
511 <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
512 <path d="M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/>
513 </svg>
514 ),
515 threads: (
516 <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
517 <path d="M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.028-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.589 12c.027 3.086.718 5.496 2.057 7.164 1.43 1.783 3.631 2.698 6.54 2.717 2.623-.02 4.358-.631 5.8-2.045 1.647-1.613 1.618-3.593 1.09-4.798-.31-.71-.873-1.3-1.634-1.75-.192 1.352-.622 2.446-1.284 3.272-.886 1.102-2.14 1.704-3.73 1.79-1.202.065-2.361-.218-3.259-.801-1.063-.689-1.685-1.74-1.752-2.964-.065-1.19.408-2.285 1.33-3.082.88-.76 2.119-1.207 3.583-1.291a13.853 13.853 0 013.271.165c-.07-.75-.273-1.318-.614-1.716-.434-.507-1.119-.768-2.036-.777h-.045c-.67 0-1.938.181-2.669 1.4l-1.812-.755c.97-1.868 2.836-2.677 4.484-2.677h.06c3.233.03 5.164 2.01 5.34 5.49.208 3.394-1.112 5.49-3.317 6.548-.384.186-.785.34-1.197.461C16.418 23.61 14.397 24 12.186 24z"/>
518 </svg>
519 ),
520 bluesky: (
521 <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
522 <path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 01-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.204-.659-.299-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8z"/>
523 </svg>
524 ),
525 mastodon: (
526 <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
527 <path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 00.023-.043v-1.809a.052.052 0 00-.066-.051c-1.517.363-3.072.546-4.632.546-2.685 0-3.463-1.284-3.674-1.818a5.593 5.593 0 01-.319-1.433.053.053 0 01.066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.124-.01 1.554-.043 3.19-.167 4.72-.498.038-.009.075-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.005-.109.033-1.25.033-1.36 0-.43.138-3.032-.019-4.643zm-3.22 9.214h-2.058V9.47c0-1.063-.447-1.601-1.35-1.601-1 0-1.5.647-1.5 1.923v2.786h-2.048V9.792c0-1.276-.5-1.923-1.5-1.923-.903 0-1.35.538-1.35 1.601v5.04H7.884V9.32c0-1.062.27-1.907.81-2.534.558-.627 1.287-.948 2.192-.948 1.047 0 1.84.402 2.363 1.206l.509.855.51-.855c.523-.804 1.316-1.206 2.363-1.206.904 0 1.633.32 2.192.948.54.627.925 1.472.925 2.534v5.19z"/>
528 </svg>
529 ),
530 };
531
532 const SOCIAL_ORDER = [ 'twitter', 'facebook', 'linkedin', 'instagram', 'tiktok', 'pinterest', 'youtube', 'threads', 'bluesky', 'mastodon' ];
533 const SOCIAL_SHARING_ORDER = [ 'twitter', 'facebook', 'linkedin', 'pinterest', 'mastodon', 'threads', 'bluesky' ];
534
535 function SocialLinksPreview( { element } ) {
536 const s = element.settings || {};
537 const headline = s.headline || 'Follow Now';
538 const align = s.align || 'left';
539 const activeIcons = SOCIAL_ORDER.filter( ( key ) => !! s[ key ] );
540
541 return (
542 <div className="bp-element-preview__social-links" style={ { textAlign: align } }>
543 { headline && (
544 <p className="bp-element-preview__social-headline">{ headline }</p>
545 ) }
546 { activeIcons.length > 0 && (
547 <div className="bp-element-preview__social-icons" style={ { justifyContent: align === 'center' ? 'center' : align === 'right' ? 'flex-end' : 'flex-start' } }>
548 { activeIcons.map( ( key ) => (
549 <span key={ key } className="bp-element-preview__social-icon" title={ key }>
550 { SOCIAL_ICONS[ key ] }
551 </span>
552 ) ) }
553 </div>
554 ) }
555 </div>
556 );
557 }
558
559 function SocialSharingPreview( { element } ) {
560 const s = element.settings || {};
561 const headline = s.headline !== undefined && s.headline !== null ? s.headline : 'Share Now';
562 const align = s.align || 'left';
563 const activeIcons = SOCIAL_SHARING_ORDER.filter( ( key ) => s[ key ] !== false );
564
565 return (
566 <div className="bp-element-preview__social-links" style={ { textAlign: align } }>
567 { headline && (
568 <p className="bp-element-preview__social-headline">{ headline }</p>
569 ) }
570 { activeIcons.length > 0 && (
571 <div className="bp-element-preview__social-icons" style={ { justifyContent: align === 'center' ? 'center' : align === 'right' ? 'flex-end' : 'flex-start' } }>
572 { activeIcons.map( ( key ) => (
573 <span key={ key } className="bp-element-preview__social-icon" title={ key }>
574 { SOCIAL_ICONS[ key ] }
575 </span>
576 ) ) }
577 </div>
578 ) }
579 </div>
580 );
581 }
582
583 // ── Campaign Title preview ────────────────────────────────────────────────────
584
585 function CampaignTitlePreview( { element, meta } ) {
586 const s = element.settings || {};
587 const title = meta?.title ?? '';
588 const align = s.align || 'left';
589
590 return (
591 <h2
592 className="bp-element-preview__campaign-title"
593 style={ { textAlign: align } }
594 >
595 { title }
596 </h2>
597 );
598 }
599
600 // ── Donate Amount preview ─────────────────────────────────────────────────────
601
602 function hexToRgba( hex, alpha ) {
603 if ( ! hex || hex.length < 7 ) return `rgba(107,99,246,${ alpha })`;
604 const r = parseInt( hex.slice( 1, 3 ), 16 );
605 const g = parseInt( hex.slice( 3, 5 ), 16 );
606 const b = parseInt( hex.slice( 5, 7 ), 16 );
607 return `rgba(${ r },${ g },${ b },${ alpha })`;
608 }
609
610 function DonateAmountPreview( { element, meta, dispatch } ) {
611 const s = element.settings || {};
612 const headline = s.headline || '';
613 const amounts = Array.isArray( meta?.bpc_suggested_amounts ) ? meta.bpc_suggested_amounts : [];
614 const allowCustom = !! meta?.bpc_allow_custom_amount;
615 const primary = meta?.bpc_color_primary || '#6b63f6';
616
617 const globalCurrencySymbol = getGlobalCurrencySymbol();
618
619 const [ customValue, setCustomValue ] = useState( '' );
620
621 const deselectAll = () =>
622 dispatch( {
623 type: 'UPDATE_META',
624 payload: {
625 bpc_suggested_amounts: amounts.map( ( a ) => ( { ...a, is_default: false } ) ),
626 },
627 } );
628
629 const handleSelectAmount = ( id, e ) => {
630 e.stopPropagation();
631 setCustomValue( '' );
632 dispatch( {
633 type: 'UPDATE_META',
634 payload: {
635 bpc_suggested_amounts: amounts.map( ( a ) => ( { ...a, is_default: a.id === id } ) ),
636 },
637 } );
638 };
639
640 const handleCustomFocus = ( e ) => {
641 e.stopPropagation();
642 deselectAll();
643 };
644
645 const handleCustomChange = ( e ) => {
646 setCustomValue( e.target.value );
647 };
648
649 if ( amounts.length === 0 && ! allowCustom ) {
650 return (
651 <div className="bp-element-preview__donate-empty">
652 <span className="dashicons dashicons-money-alt" />
653 <span>{ __( 'No suggested amounts yet — add some in the element settings', 'better-payment' ) }</span>
654 </div>
655 );
656 }
657
658 const anySelected = amounts.some( ( a ) => a.is_default );
659 const customActive = allowCustom && ! anySelected && customValue !== '';
660
661 return (
662 <div className="bp-element-preview__donate" style={ { pointerEvents: 'none' } }>
663 { headline && (
664 <p className="bp-element-preview__donate-headline">{ headline }</p>
665 ) }
666
667 { amounts.length > 0 && (
668 <div className="bp-element-preview__donate-amounts">
669 { amounts.map( ( item ) => {
670 const selected = !! item.is_default;
671 const btnStyle = selected
672 ? {
673 borderColor: primary,
674 color: primary,
675 backgroundColor: hexToRgba( primary, 0.08 ),
676 }
677 : {};
678 return (
679 <button
680 key={ item.id }
681 type="button"
682 className={ 'bp-element-preview__donate-btn' + ( selected ? ' is-selected' : '' ) }
683 style={ btnStyle }
684 onClick={ ( e ) => handleSelectAmount( item.id, e ) }
685 >
686 { globalCurrencySymbol }{ item.amount }
687 </button>
688 );
689 } ) }
690 </div>
691 ) }
692
693 { allowCustom && (
694 <div
695 className={ 'bp-element-preview__donate-custom' + ( customActive ? ' is-active' : '' ) }
696 style={ customActive ? { borderColor: primary, boxShadow: `0 0 0 2px ${ hexToRgba( primary, 0.18 ) }` } : {} }
697 onClick={ ( e ) => e.stopPropagation() }
698 >
699 <span className="bp-element-preview__donate-custom-prefix">{ globalCurrencySymbol }</span>
700 <input
701 type="number"
702 min="0"
703 step="1"
704 value={ customValue }
705 placeholder={ __( 'Enter custom amount', 'better-payment' ) }
706 onChange={ handleCustomChange }
707 onFocus={ handleCustomFocus }
708 />
709 </div>
710 ) }
711 </div>
712 );
713 }
714
715 // ── Donation Form (Donate Button) preview ─────────────────────────────────────
716
717 function DonationFormPreview( { element, meta } ) {
718 const s = element.settings || {};
719 const label = s.button_label || s.button_text || 'Donate Now';
720 const primary = meta?.bpc_color_primary || '#6b63f6';
721 const buttonColor = s.button_color || primary;
722 const width = s.width ?? 100;
723 const align = s.align || 'center';
724
725 const wrapStyle = {
726 width: `${ width }%`,
727 margin: align === 'center' ? '0 auto' : align === 'right' ? '0 0 0 auto' : '0',
728 };
729
730 return (
731 <div className="bp-element-preview__donate-button-wrap" style={ wrapStyle }>
732 <span
733 className="bp-element-preview__donate-button-btn"
734 style={ { backgroundColor: buttonColor } }
735 >
736 { label }
737 </span>
738 </div>
739 );
740 }
741
742 // ── Organizer preview ─────────────────────────────────────────────────────────
743
744 function OrganizerPreview( { element } ) {
745 const s = element.settings || {};
746 const roleTitle = s.role_title || 'Organizer';
747 const description = s.description || '';
748 const width = s.width ?? 100;
749 const align = s.align || 'left';
750
751 // Resolve display name + avatar from the localized element registry options.
752 const def = getElement( 'organizer' );
753 const creatorOptions = def?.settingsSchema?.find( ( c ) => c.key === 'creator_user_id' )?.options || [];
754 const creator = creatorOptions.find( ( o ) => String( o.value ) === String( s.creator_user_id ) );
755 const displayName = creator?.label || '';
756 const avatarUrl = creator?.avatar_url || '';
757
758 const wrapStyle = {
759 width: `${ width }%`,
760 margin: align === 'center' ? '0 auto' : align === 'right' ? '0 0 0 auto' : '0',
761 };
762
763 return (
764 <div className="bp-element-preview__organizer" style={ wrapStyle }>
765 <div className="bp-element-preview__organizer-avatar">
766 { avatarUrl
767 ? <img src={ avatarUrl } alt={ displayName } />
768 : <span className="dashicons dashicons-admin-users" />
769 }
770 </div>
771 <div className="bp-element-preview__organizer-info">
772 { displayName && (
773 <span className="bp-element-preview__organizer-name">{ displayName }</span>
774 ) }
775 { roleTitle && (
776 <span className="bp-element-preview__organizer-role">{ roleTitle }</span>
777 ) }
778 { description && (
779 <div
780 className="bp-element-preview__organizer-desc"
781 dangerouslySetInnerHTML={ { __html: scaleInlineFontSizes( description ) } }
782 />
783 ) }
784 </div>
785 </div>
786 );
787 }
788