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

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

387 lines 21.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { useState } from '@wordpress/element';
2 import { __ } from '@wordpress/i18n';
3
4 // ── Category definitions (below the "All Templates" divider) ──────────────────
5 const CATEGORIES = [
6 { key: 'charity', label: __( 'Charity', 'better-payment' ) },
7 { key: 'club-organizations', label: __( 'Club / Organizations', 'better-payment' ) },
8 { key: 'environmental', label: __( 'Environmental', 'better-payment' ) },
9 { key: 'medical', label: __( 'Medical', 'better-payment' ) },
10 { key: 'education', label: __( 'Youth / Education', 'better-payment' ) },
11 ];
12
13 // ── Blank-layout SVG wireframes ───────────────────────────────────────────────
14 const BLANK_ICONS = {
15 'blank-1col': (
16 <svg width="120" height="90" viewBox="0 0 120 90" fill="none">
17 <rect x="8" y="8" width="104" height="74" rx="4" fill="#f5f5f5" stroke="#e5e7eb" strokeWidth="1.5"/>
18 <rect x="16" y="16" width="88" height="10" rx="3" fill="#f59e0b" opacity="0.5"/>
19 <rect x="16" y="32" width="88" height="6" rx="2" fill="#d1d5db"/>
20 <rect x="16" y="42" width="70" height="6" rx="2" fill="#d1d5db"/>
21 <rect x="16" y="52" width="80" height="6" rx="2" fill="#d1d5db"/>
22 <rect x="16" y="66" width="30" height="8" rx="2" fill="#f59e0b" opacity="0.7"/>
23 </svg>
24 ),
25 'blank-2col': (
26 <svg width="120" height="90" viewBox="0 0 120 90" fill="none">
27 <rect x="8" y="8" width="104" height="74" rx="4" fill="#f5f5f5" stroke="#e5e7eb" strokeWidth="1.5"/>
28 <rect x="16" y="16" width="58" height="10" rx="3" fill="#f59e0b" opacity="0.5"/>
29 <rect x="16" y="32" width="58" height="6" rx="2" fill="#d1d5db"/>
30 <rect x="16" y="42" width="46" height="6" rx="2" fill="#d1d5db"/>
31 <rect x="16" y="52" width="58" height="6" rx="2" fill="#d1d5db"/>
32 <rect x="16" y="66" width="24" height="8" rx="2" fill="#f59e0b" opacity="0.7"/>
33 <rect x="82" y="16" width="22" height="58" rx="3" fill="#f59e0b" opacity="0.12" stroke="#f59e0b" strokeWidth="1.2"/>
34 <rect x="86" y="22" width="14" height="4" rx="2" fill="#d1d5db"/>
35 <rect x="86" y="30" width="10" height="4" rx="2" fill="#d1d5db"/>
36 <rect x="86" y="38" width="12" height="4" rx="2" fill="#d1d5db"/>
37 </svg>
38 ),
39 'blank-3col': (
40 <svg width="120" height="90" viewBox="0 0 120 90" fill="none">
41 <rect x="8" y="8" width="104" height="74" rx="4" fill="#f5f5f5" stroke="#e5e7eb" strokeWidth="1.5"/>
42 <rect x="16" y="16" width="28" height="58" rx="3" fill="#f59e0b" opacity="0.12" stroke="#f59e0b" strokeWidth="1.2"/>
43 <rect x="19" y="22" width="22" height="4" rx="2" fill="#d1d5db"/>
44 <rect x="19" y="30" width="16" height="4" rx="2" fill="#d1d5db"/>
45 <rect x="19" y="38" width="20" height="4" rx="2" fill="#d1d5db"/>
46 <rect x="50" y="16" width="20" height="58" rx="3" fill="#f59e0b" opacity="0.12" stroke="#f59e0b" strokeWidth="1.2"/>
47 <rect x="53" y="22" width="14" height="4" rx="2" fill="#d1d5db"/>
48 <rect x="53" y="30" width="10" height="4" rx="2" fill="#d1d5db"/>
49 <rect x="53" y="38" width="12" height="4" rx="2" fill="#d1d5db"/>
50 <rect x="76" y="16" width="28" height="58" rx="3" fill="#f59e0b" opacity="0.12" stroke="#f59e0b" strokeWidth="1.2"/>
51 <rect x="79" y="22" width="22" height="4" rx="2" fill="#d1d5db"/>
52 <rect x="79" y="30" width="16" height="4" rx="2" fill="#d1d5db"/>
53 <rect x="79" y="38" width="20" height="4" rx="2" fill="#d1d5db"/>
54 </svg>
55 ),
56 };
57
58 export default function TemplatesTab( {
59 dispatch,
60 appliedTemplateKey,
61 onTemplateApplied,
62 } ) {
63 const data = window.betterPaymentCampaignData || {};
64 const templates = data.templates || [];
65 const pluginUrl = ( data.pluginUrl || '' ).replace( /\/$/, '' );
66
67 const blankTemplates = templates.filter( ( t ) => t.category === 'blank' );
68 const prebuiltTemplates = templates.filter( ( t ) => t.category === 'prebuilt' );
69
70 const [ search, setSearch ] = useState( '' );
71 const [ activeCategory, setActiveCategory ] = useState( 'all' );
72 const [ lightboxKey, setLightboxKey ] = useState( null );
73 const [ confirmTemplate, setConfirmTemplate ] = useState( null ); // template pending confirmation
74
75 const handleSelect = ( template ) => {
76 // If a template is already applied and the user is switching to a different one,
77 // show a confirmation modal before overwriting the current layout.
78 if ( appliedTemplateKey && template.key !== appliedTemplateKey ) {
79 setConfirmTemplate( template );
80 return;
81 }
82 applyTemplate( template );
83 };
84
85 const applyTemplate = ( template ) => {
86 dispatch( { type: 'APPLY_TEMPLATE', template } );
87 onTemplateApplied();
88 };
89
90 const filteredPrebuilt = prebuiltTemplates.filter( ( tpl ) => {
91 const matchesCat = activeCategory === 'all' || ( tpl.tags || [] ).includes( activeCategory );
92 const matchesSearch = ! search || tpl.label.toLowerCase().includes( search.toLowerCase() ) || ( tpl.description || '' ).toLowerCase().includes( search.toLowerCase() );
93 return matchesCat && matchesSearch;
94 } );
95
96 const filteredBlank = blankTemplates.filter( ( tpl ) => {
97 if ( activeCategory !== 'all' ) return false;
98 return ! search || tpl.label.toLowerCase().includes( search.toLowerCase() );
99 } );
100
101 const getCategoryCount = ( key ) =>
102 prebuiltTemplates.filter( ( t ) => ( t.tags || [] ).includes( key ) ).length;
103
104 const currentLabel = appliedTemplateKey
105 ? ( templates.find( ( t ) => t.key === appliedTemplateKey )?.label || '' )
106 : null;
107
108 const getPreviewImageUrl = ( tpl ) => {
109 if ( ! tpl.preview_image ) return '';
110 return `${ pluginUrl }/${ tpl.preview_image }`;
111 };
112
113 const lightboxTpl = lightboxKey ? templates.find( ( t ) => t.key === lightboxKey ) : null;
114
115 return (
116 <div className="bp-templates-tab">
117
118 {/* Page header */}
119 <div className="bp-templates-tab__page-header">
120 <h2>{ __( 'Select A Template', 'better-payment' ) }</h2>
121 { currentLabel && (
122 <p>
123 { __( 'You are currently using the', 'better-payment' ) }{ ' ' }
124 <strong>"{ currentLabel }"</strong>{ ' ' }
125 { __( 'template. Changing a template now might result in losing fields and data. Proceed carefully.', 'better-payment' ) }
126 </p>
127 ) }
128 </div>
129
130 <div className="bp-templates-tab__body">
131
132 {/* Sidebar */}
133 <div className="bp-templates-tab__sidebar">
134
135 <div className="bp-templates-tab__search">
136 <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
137 <circle cx="6" cy="6" r="4.5" stroke="#8a8fa6" strokeWidth="1.3"/>
138 <path d="M9.5 9.5L12.5 12.5" stroke="#8a8fa6" strokeWidth="1.3" strokeLinecap="round"/>
139 </svg>
140 <input
141 type="text"
142 placeholder={ __( 'Search Templates', 'better-payment' ) }
143 value={ search }
144 onChange={ ( e ) => setSearch( e.target.value ) }
145 />
146 </div>
147
148 <button
149 className={ `bp-templates-tab__all-cat${ activeCategory === 'all' ? ' is-active' : '' }` }
150 onClick={ () => setActiveCategory( 'all' ) }
151 >
152 <span>{ __( 'All Templates', 'better-payment' ) }</span>
153 <span className="bp-templates-tab__cat-count">{ templates.length }</span>
154 </button>
155
156 <hr className="bp-templates-tab__divider" />
157
158 <div className="bp-templates-tab__cats">
159 { CATEGORIES.map( ( cat ) => {
160 const count = getCategoryCount( cat.key );
161 if ( count === 0 ) return null;
162 return (
163 <button
164 key={ cat.key }
165 className={ `bp-templates-tab__cat${ activeCategory === cat.key ? ' is-active' : '' }` }
166 onClick={ () => setActiveCategory( cat.key ) }
167 >
168 <span>{ cat.label }</span>
169 <span className="bp-templates-tab__cat-num">{ count }</span>
170 </button>
171 );
172 } ) }
173 </div>
174
175 <div className="bp-templates-tab__sidebar-cta">
176 <strong>{ __( "Don’t See The Template You’re Looking For?", 'better-payment' ) }</strong>
177 <p>
178 <a href="https://wpdeveloper.com/support/" target='_blank'>{ __( 'Share', 'better-payment' ) }</a>
179 { ' ' }{ __( 'your suggestions for future additions.', 'better-payment' ) }
180 </p>
181 </div>
182 </div>
183
184 {/* Main grid */}
185 <div className="bp-templates-tab__main">
186
187 { filteredBlank.length > 0 && (
188 <div className="bp-templates-tab__section">
189 { activeCategory === 'all' && ! search && (
190 <p className="bp-templates-tab__section-heading">
191 { __( 'Start with a blank template', 'better-payment' ) }
192 </p>
193 ) }
194 <div className="bp-templates-tab__grid">
195 { filteredBlank.map( ( tpl ) => {
196 const isActive = tpl.key === appliedTemplateKey;
197 return (
198 <button
199 key={ tpl.key }
200 className={ `bp-tpl-card${ isActive ? ' is-active' : '' }` }
201 onClick={ () => ! isActive && handleSelect( tpl ) }
202 >
203 { isActive && (
204 <span className="bp-tpl-card__badge">{ __( 'Active', 'better-payment' ) }</span>
205 ) }
206 <div className="bp-tpl-card__thumb bp-tpl-card__thumb--blank">
207 { BLANK_ICONS[ tpl.key ] || <span className="dashicons dashicons-layout" /> }
208 { ! isActive && (
209 <div className="bp-tpl-card__hover">
210 <span className="bp-tpl-card__hover-btn bp-tpl-card__hover-btn--primary">
211 { __( 'Use Template', 'better-payment' ) }
212 </span>
213 </div>
214 ) }
215 </div>
216 <div className="bp-tpl-card__info">
217 <strong>{ tpl.label }</strong>
218 { tpl.description && <span>{ tpl.description }</span> }
219 </div>
220 </button>
221 );
222 } ) }
223 </div>
224 </div>
225 ) }
226
227 { filteredPrebuilt.length > 0 && (
228 <div className="bp-templates-tab__section">
229 { activeCategory === 'all' && ! search && (
230 <p className="bp-templates-tab__section-heading">
231 { __( 'Or choose a pre-built template and launch your campaign in minutes!', 'better-payment' ) }
232 </p>
233 ) }
234 <div className="bp-templates-tab__grid">
235 { filteredPrebuilt.map( ( tpl ) => {
236 const isActive = tpl.key === appliedTemplateKey;
237 const imgUrl = getPreviewImageUrl( tpl );
238 return (
239 <div
240 key={ tpl.key }
241 className={ `bp-tpl-card${ isActive ? ' is-active' : '' }` }
242 >
243 { isActive && (
244 <span className="bp-tpl-card__badge">{ __( 'Active', 'better-payment' ) }</span>
245 ) }
246 <div className="bp-tpl-card__thumb">
247 { imgUrl ? (
248 <img
249 src={ imgUrl }
250 alt={ tpl.label }
251 className="bp-tpl-card__screenshot"
252 draggable={ false }
253 />
254 ) : (
255 <div className="bp-tpl-card__thumb-placeholder" style={ { background: tpl.preview_color || '#6b63f6' } } />
256 ) }
257 { ! isActive && (
258 <div className="bp-tpl-card__hover">
259 <button
260 className="bp-tpl-card__hover-btn bp-tpl-card__hover-btn--primary"
261 onClick={ () => handleSelect( tpl ) }
262 >
263 { __( 'Use Template', 'better-payment' ) }
264 </button>
265 <button
266 className="bp-tpl-card__hover-btn"
267 onClick={ ( e ) => { e.stopPropagation(); setLightboxKey( tpl.key ); } }
268 >
269 { __( 'Preview', 'better-payment' ) }
270 </button>
271 </div>
272 ) }
273 </div>
274 <div className="bp-tpl-card__info">
275 <strong>{ tpl.label }</strong>
276 { tpl.description && <span>{ tpl.description }</span> }
277 </div>
278 </div>
279 );
280 } ) }
281 </div>
282 </div>
283 ) }
284
285 { filteredBlank.length === 0 && filteredPrebuilt.length === 0 && (
286 <div className="bp-templates-tab__empty">
287 <p>{ __( 'No templates match your search.', 'better-payment' ) }</p>
288 </div>
289 ) }
290 </div>
291 </div>
292
293 {/* Lightbox — shows static screenshot */}
294 { lightboxTpl && (
295 <div className="bp-template-lightbox-overlay" onClick={ () => setLightboxKey( null ) }>
296 <div className="bp-template-lightbox" onClick={ ( e ) => e.stopPropagation() }>
297
298 {/* Minimal close — absolute top-right, no background */}
299 <button
300 className="bp-template-lightbox__close-btn"
301 onClick={ () => setLightboxKey( null ) }
302 aria-label={ __( 'Close', 'better-payment' ) }
303 >
304 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
305 <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
306 </svg>
307 </button>
308
309 {/* Header — large title + description, no border */}
310 <div className="bp-template-lightbox__header">
311 <h2 className="bp-template-lightbox__title">{ lightboxTpl.label }</h2>
312 { lightboxTpl.description && (
313 <p className="bp-template-lightbox__desc">{ lightboxTpl.description }</p>
314 ) }
315 </div>
316
317 {/* Scrollable image area */}
318 <div className="bp-template-lightbox__body">
319 { getPreviewImageUrl( lightboxTpl ) ? (
320 <img
321 src={ getPreviewImageUrl( lightboxTpl ) }
322 alt={ lightboxTpl.label }
323 className="bp-template-lightbox__img"
324 />
325 ) : (
326 <div className="bp-template-lightbox__no-preview">
327 { __( 'No preview available.', 'better-payment' ) }
328 </div>
329 ) }
330 </div>
331
332 {/* Full-width footer button */}
333 <button
334 className="bp-template-lightbox__use-btn"
335 onClick={ () => { handleSelect( lightboxTpl ); setLightboxKey( null ); } }
336 >
337 { __( 'Use This Template', 'better-payment' ) }
338 </button>
339
340 </div>
341 </div>
342 ) }
343
344 {/* Template change confirmation modal */}
345 { confirmTemplate && (
346 <div className="bp-template-confirm-overlay" onClick={ () => setConfirmTemplate( null ) }>
347 <div className="bp-template-confirm" onClick={ ( e ) => e.stopPropagation() }>
348 <div className="bp-template-confirm__icon">
349 <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
350 <path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/>
351 <line x1="12" y1="9" x2="12" y2="13"/>
352 <line x1="12" y1="17" x2="12.01" y2="17"/>
353 </svg>
354 </div>
355 <h3 className="bp-template-confirm__heading">
356 { __( 'Switch Template?', 'better-payment' ) }
357 </h3>
358 <p className="bp-template-confirm__body">
359 { __( 'Switching to', 'better-payment' ) }{ ' ' }
360 <strong>"{ confirmTemplate.label }"</strong>{ ' ' }
361 { __( 'will replace your current layout and all its elements. This cannot be undone.', 'better-payment' ) }
362 </p>
363 <div className="bp-template-confirm__actions">
364 <button
365 className="bp-template-confirm__btn bp-template-confirm__btn--cancel"
366 onClick={ () => setConfirmTemplate( null ) }
367 >
368 { __( 'Cancel', 'better-payment' ) }
369 </button>
370 <button
371 className="bp-template-confirm__btn bp-template-confirm__btn--confirm"
372 onClick={ () => {
373 applyTemplate( confirmTemplate );
374 setConfirmTemplate( null );
375 } }
376 >
377 { __( 'Yes, Switch Template', 'better-payment' ) }
378 </button>
379 </div>
380 </div>
381 </div>
382 ) }
383
384 </div>
385 );
386 }
387