PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / 2.2.0
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More v2.2.0
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.0, at src/campaign-builder/components/ElementCard.js

790 lines 38.0 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 : null;
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 && goal > 0 && (
268 <div className="bp-element-preview__summary-item">
269 <strong>{ percent }%</strong>
270 <span>{ __( 'Funded', 'better-payment' ) }</span>
271 </div>
272 ) }
273 { showDays && days !== null && (
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 = goal > 0
364 ? ( currencySymbol + ( roundAmounts ? Math.ceil( goal ).toLocaleString() : Number( goal ).toFixed( 2 ) ) )
365 : __( 'No goal set', 'better-payment' );
366
367 return (
368 <div className="bp-element-preview__progress" style={ wrapStyle }>
369 { headline && (
370 <p className="bp-element-preview__progress-headline">{ headline }</p>
371 ) }
372 <div className="bp-element-preview__progress-track">
373 <div
374 className="bp-element-preview__progress-fill"
375 style={ { width: `${ progress }%`, backgroundColor: primary } }
376 />
377 </div>
378 { ( showDonated || showGoal ) && (
379 <div className="bp-element-preview__progress-labels">
380 { showDonated && (
381 <span className="bp-element-preview__progress-donated">
382 { donateLabel } { displayProgress }%
383 </span>
384 ) }
385 { showGoal && (
386 <span className="bp-element-preview__progress-goal">
387 { goalLabel } { goalFormatted }
388 </span>
389 ) }
390 </div>
391 ) }
392 </div>
393 );
394 }
395
396 // ── Photo preview ─────────────────────────────────────────────────────────────
397
398 function PhotoPreview( { element, columnId, dispatch } ) {
399 const s = element.settings || {};
400
401 const handleImageClick = useCallback( ( e ) => {
402 e.stopPropagation();
403 openWpMedia( ( { url, id, sizes } ) => {
404 dispatch( {
405 type: 'UPDATE_ELEMENT_SETTINGS',
406 elementId: element.id,
407 settings: { src: url, src_id: id, src_sizes: sizes },
408 } );
409 } );
410 }, [ element.id, dispatch ] );
411 const src = s.src || '';
412 const alt = s.alt || '';
413 const width = s.width ?? 100;
414 const align = s.align || 'center';
415 const size = s.size || 'full';
416 const sizes = s.src_sizes || {};
417 const srcId = s.src_id || 0;
418
419 // Keyed by attachment ID — fetched once per ID, cached for all size changes.
420 const [ fetchedSizes, setFetchedSizes ] = useState( {} );
421
422 useEffect( () => {
423 if ( ! srcId ) {
424 setFetchedSizes( {} );
425 return;
426 }
427 let cancelled = false;
428 apiFetch( { path: `/wp/v2/media/${ srcId }` } )
429 .then( ( data ) => {
430 if ( cancelled ) return;
431 const raw = data?.media_details?.sizes || {};
432 const mapped = {};
433 Object.keys( raw ).forEach( ( k ) => {
434 mapped[ k ] = raw[ k ].source_url;
435 } );
436 setFetchedSizes( mapped );
437 } )
438 .catch( () => {} );
439 return () => { cancelled = true; };
440 }, [ srcId ] );
441
442 // Priority: src_sizes (from media modal) → fetched from REST API → original src.
443 const resolvedUrl = size !== 'full'
444 ? ( sizes[ size ]?.url || fetchedSizes[ size ] || src )
445 : src;
446
447 const wrapStyle = { textAlign: align };
448 const imgStyle = {
449 width: 'auto',
450 maxWidth: `${ width }%`,
451 height: 'auto',
452 display: 'block',
453 borderRadius: '4px',
454 margin: align === 'center' ? '0 auto' : align === 'right' ? '0 0 0 auto' : '0',
455 };
456
457 if ( ! src ) {
458 return (
459 <div className="bp-element-preview__photo-empty" onClick={ handleImageClick } style={ { cursor: 'pointer' } }>
460 <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="#bbb">
461 <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"/>
462 </svg>
463 </div>
464 );
465 }
466
467 return (
468 <div className="bp-element-preview__photo" style={ wrapStyle }>
469 <img
470 src={ resolvedUrl }
471 alt={ alt }
472 style={ { ...imgStyle, cursor: 'pointer' } }
473 onClick={ handleImageClick }
474 />
475 </div>
476 );
477 }
478
479 // ── Social Links preview ──────────────────────────────────────────────────────
480
481 const SOCIAL_ICONS = {
482 twitter: (
483 <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
484 <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"/>
485 </svg>
486 ),
487 facebook: (
488 <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
489 <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"/>
490 </svg>
491 ),
492 linkedin: (
493 <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
494 <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"/>
495 </svg>
496 ),
497 instagram: (
498 <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
499 <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"/>
500 </svg>
501 ),
502 tiktok: (
503 <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
504 <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"/>
505 </svg>
506 ),
507 pinterest: (
508 <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
509 <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"/>
510 </svg>
511 ),
512 youtube: (
513 <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
514 <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"/>
515 </svg>
516 ),
517 threads: (
518 <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
519 <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"/>
520 </svg>
521 ),
522 bluesky: (
523 <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
524 <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"/>
525 </svg>
526 ),
527 mastodon: (
528 <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
529 <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"/>
530 </svg>
531 ),
532 };
533
534 const SOCIAL_ORDER = [ 'twitter', 'facebook', 'linkedin', 'instagram', 'tiktok', 'pinterest', 'youtube', 'threads', 'bluesky', 'mastodon' ];
535 const SOCIAL_SHARING_ORDER = [ 'twitter', 'facebook', 'linkedin', 'pinterest', 'mastodon', 'threads', 'bluesky' ];
536
537 function SocialLinksPreview( { element } ) {
538 const s = element.settings || {};
539 const headline = s.headline || 'Follow Now';
540 const align = s.align || 'left';
541 const activeIcons = SOCIAL_ORDER.filter( ( key ) => !! s[ key ] );
542
543 return (
544 <div className="bp-element-preview__social-links" style={ { textAlign: align } }>
545 { headline && (
546 <p className="bp-element-preview__social-headline">{ headline }</p>
547 ) }
548 { activeIcons.length > 0 && (
549 <div className="bp-element-preview__social-icons" style={ { justifyContent: align === 'center' ? 'center' : align === 'right' ? 'flex-end' : 'flex-start' } }>
550 { activeIcons.map( ( key ) => (
551 <span key={ key } className="bp-element-preview__social-icon" title={ key }>
552 { SOCIAL_ICONS[ key ] }
553 </span>
554 ) ) }
555 </div>
556 ) }
557 </div>
558 );
559 }
560
561 function SocialSharingPreview( { element } ) {
562 const s = element.settings || {};
563 const headline = s.headline !== undefined && s.headline !== null ? s.headline : 'Share Now';
564 const align = s.align || 'left';
565 const activeIcons = SOCIAL_SHARING_ORDER.filter( ( key ) => s[ key ] !== false );
566
567 return (
568 <div className="bp-element-preview__social-links" style={ { textAlign: align } }>
569 { headline && (
570 <p className="bp-element-preview__social-headline">{ headline }</p>
571 ) }
572 { activeIcons.length > 0 && (
573 <div className="bp-element-preview__social-icons" style={ { justifyContent: align === 'center' ? 'center' : align === 'right' ? 'flex-end' : 'flex-start' } }>
574 { activeIcons.map( ( key ) => (
575 <span key={ key } className="bp-element-preview__social-icon" title={ key }>
576 { SOCIAL_ICONS[ key ] }
577 </span>
578 ) ) }
579 </div>
580 ) }
581 </div>
582 );
583 }
584
585 // ── Campaign Title preview ────────────────────────────────────────────────────
586
587 function CampaignTitlePreview( { element, meta } ) {
588 const s = element.settings || {};
589 const title = meta?.title ?? '';
590 const align = s.align || 'left';
591
592 return (
593 <h2
594 className="bp-element-preview__campaign-title"
595 style={ { textAlign: align } }
596 >
597 { title }
598 </h2>
599 );
600 }
601
602 // ── Donate Amount preview ─────────────────────────────────────────────────────
603
604 function hexToRgba( hex, alpha ) {
605 if ( ! hex || hex.length < 7 ) return `rgba(107,99,246,${ alpha })`;
606 const r = parseInt( hex.slice( 1, 3 ), 16 );
607 const g = parseInt( hex.slice( 3, 5 ), 16 );
608 const b = parseInt( hex.slice( 5, 7 ), 16 );
609 return `rgba(${ r },${ g },${ b },${ alpha })`;
610 }
611
612 function DonateAmountPreview( { element, meta, dispatch } ) {
613 const s = element.settings || {};
614 const headline = s.headline || '';
615 const amounts = Array.isArray( meta?.bpc_suggested_amounts ) ? meta.bpc_suggested_amounts : [];
616 const allowCustom = !! meta?.bpc_allow_custom_amount;
617 const primary = meta?.bpc_color_primary || '#6b63f6';
618
619 const globalCurrencySymbol = getGlobalCurrencySymbol();
620
621 const [ customValue, setCustomValue ] = useState( '' );
622
623 const deselectAll = () =>
624 dispatch( {
625 type: 'UPDATE_META',
626 payload: {
627 bpc_suggested_amounts: amounts.map( ( a ) => ( { ...a, is_default: false } ) ),
628 },
629 } );
630
631 const handleSelectAmount = ( id, e ) => {
632 e.stopPropagation();
633 setCustomValue( '' );
634 dispatch( {
635 type: 'UPDATE_META',
636 payload: {
637 bpc_suggested_amounts: amounts.map( ( a ) => ( { ...a, is_default: a.id === id } ) ),
638 },
639 } );
640 };
641
642 const handleCustomFocus = ( e ) => {
643 e.stopPropagation();
644 deselectAll();
645 };
646
647 const handleCustomChange = ( e ) => {
648 setCustomValue( e.target.value );
649 };
650
651 if ( amounts.length === 0 && ! allowCustom ) {
652 return (
653 <div className="bp-element-preview__donate-empty">
654 <span className="dashicons dashicons-money-alt" />
655 <span>{ __( 'No suggested amounts yet — add some in Donation Options', 'better-payment' ) }</span>
656 </div>
657 );
658 }
659
660 const anySelected = amounts.some( ( a ) => a.is_default );
661 const customActive = allowCustom && ! anySelected && customValue !== '';
662
663 return (
664 <div className="bp-element-preview__donate" style={ { pointerEvents: 'none' } }>
665 { headline && (
666 <p className="bp-element-preview__donate-headline">{ headline }</p>
667 ) }
668
669 { amounts.length > 0 && (
670 <div className="bp-element-preview__donate-amounts">
671 { amounts.map( ( item ) => {
672 const selected = !! item.is_default;
673 const btnStyle = selected
674 ? {
675 borderColor: primary,
676 color: primary,
677 backgroundColor: hexToRgba( primary, 0.08 ),
678 }
679 : {};
680 return (
681 <button
682 key={ item.id }
683 type="button"
684 className={ 'bp-element-preview__donate-btn' + ( selected ? ' is-selected' : '' ) }
685 style={ btnStyle }
686 onClick={ ( e ) => handleSelectAmount( item.id, e ) }
687 >
688 { globalCurrencySymbol }{ item.amount }
689 </button>
690 );
691 } ) }
692 </div>
693 ) }
694
695 { allowCustom && (
696 <div
697 className={ 'bp-element-preview__donate-custom' + ( customActive ? ' is-active' : '' ) }
698 style={ customActive ? { borderColor: primary, boxShadow: `0 0 0 2px ${ hexToRgba( primary, 0.18 ) }` } : {} }
699 onClick={ ( e ) => e.stopPropagation() }
700 >
701 <span className="bp-element-preview__donate-custom-prefix">{ globalCurrencySymbol }</span>
702 <input
703 type="number"
704 min="0"
705 step="1"
706 value={ customValue }
707 placeholder={ __( 'Enter custom amount', 'better-payment' ) }
708 onChange={ handleCustomChange }
709 onFocus={ handleCustomFocus }
710 />
711 </div>
712 ) }
713 </div>
714 );
715 }
716
717 // ── Donation Form (Donate Button) preview ─────────────────────────────────────
718
719 function DonationFormPreview( { element, meta } ) {
720 const s = element.settings || {};
721 const label = s.button_label || s.button_text || 'Donate Now';
722 const primary = meta?.bpc_color_primary || '#6b63f6';
723 const buttonColor = s.button_color || primary;
724 const width = s.width ?? 100;
725 const align = s.align || 'center';
726
727 const wrapStyle = {
728 width: `${ width }%`,
729 margin: align === 'center' ? '0 auto' : align === 'right' ? '0 0 0 auto' : '0',
730 };
731
732 return (
733 <div className="bp-element-preview__donate-button-wrap" style={ wrapStyle }>
734 <span
735 className="bp-element-preview__donate-button-btn"
736 style={ { backgroundColor: buttonColor } }
737 >
738 { label }
739 </span>
740 </div>
741 );
742 }
743
744 // ── Organizer preview ─────────────────────────────────────────────────────────
745
746 function OrganizerPreview( { element } ) {
747 const s = element.settings || {};
748 const roleTitle = s.role_title || 'Organizer';
749 const description = s.description || '';
750 const width = s.width ?? 100;
751 const align = s.align || 'left';
752
753 // Resolve display name + avatar from the localized element registry options.
754 const def = getElement( 'organizer' );
755 const creatorOptions = def?.settingsSchema?.find( ( c ) => c.key === 'creator_user_id' )?.options || [];
756 const creator = creatorOptions.find( ( o ) => String( o.value ) === String( s.creator_user_id ) );
757 const displayName = creator?.label || '';
758 const avatarUrl = creator?.avatar_url || '';
759
760 const wrapStyle = {
761 width: `${ width }%`,
762 margin: align === 'center' ? '0 auto' : align === 'right' ? '0 0 0 auto' : '0',
763 };
764
765 return (
766 <div className="bp-element-preview__organizer" style={ wrapStyle }>
767 <div className="bp-element-preview__organizer-avatar">
768 { avatarUrl
769 ? <img src={ avatarUrl } alt={ displayName } />
770 : <span className="dashicons dashicons-admin-users" />
771 }
772 </div>
773 <div className="bp-element-preview__organizer-info">
774 { displayName && (
775 <span className="bp-element-preview__organizer-name">{ displayName }</span>
776 ) }
777 { roleTitle && (
778 <span className="bp-element-preview__organizer-role">{ roleTitle }</span>
779 ) }
780 { description && (
781 <div
782 className="bp-element-preview__organizer-desc"
783 dangerouslySetInnerHTML={ { __html: scaleInlineFontSizes( description ) } }
784 />
785 ) }
786 </div>
787 </div>
788 );
789 }
790