PluginProbe ʕ •ᴥ•ʔ
Starter Sites & Templates by Neve / trunk
Starter Sites & Templates by Neve vtrunk
1.4.1 1.4.0 1.3.0 1.2.29 1.2.28 1.2.6 1.2.7 1.2.8 1.2.9 trunk 1.0.10 1.0.11 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.10 1.1.11 1.1.12 1.1.13 1.1.14 1.1.15 1.1.16 1.1.17 1.1.18 1.1.19 1.1.2 1.1.20 1.1.21 1.1.22 1.1.23 1.1.24 1.1.25 1.1.26 1.1.27 1.1.28 1.1.29 1.1.3 1.1.30 1.1.31 1.1.32 1.1.33 1.1.34 1.1.35 1.1.36 1.1.37 1.1.38 1.1.39 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 1.2.17 1.2.18 1.2.19 1.2.2 1.2.20 1.2.21 1.2.22 1.2.23 1.2.24 1.2.25 1.2.26 1.2.27 1.2.3 1.2.4 1.2.5
templates-patterns-collection / onboarding / src / Components / Steps / SiteList.js
templates-patterns-collection / onboarding / src / Components / Steps Last commit date
CustomizeSite.js 1 year ago Import.js 3 weeks ago SiteList.js 2 weeks ago Welcome.js 2 years ago
SiteList.js
299 lines
1 /* global tiobDash */
2 import { __ } from '@wordpress/i18n';
3 import {
4 createInterpolateElement,
5 useEffect,
6 useState,
7 } from '@wordpress/element';
8 import { compose } from '@wordpress/compose';
9 import { withDispatch, withSelect } from '@wordpress/data';
10 import Toast from '../Toast';
11 import Filters from '../Filters';
12 import Sites from '../Sites';
13 import EditorSelector from '../EditorSelector';
14 import SVG from '../../utils/svg';
15 import { get, track } from '../../utils/rest';
16
17 const { onboarding } = tiobDash;
18
19 // Debounce the search so it stays off the network while typing: only fire after the
20 // field is idle for SEARCH_DEBOUNCE_MS and the query is at least SEARCH_MIN_CHARS.
21 const SEARCH_DEBOUNCE_MS = 700;
22 const SEARCH_MIN_CHARS = 3;
23
24 // Guards tracking-session init against firing twice while the first request is in flight.
25 let trackingInitStarted = false;
26
27 const SiteList = ( {
28 showToast,
29 setShowToast,
30 editor,
31 searchQuery,
32 trackingId,
33 setTrackingId,
34 setRankedOrder,
35 setSearchOrder,
36 setSearchFailed,
37 } ) => {
38 const [ personalizing, setPersonalizing ] = useState( false );
39 const [ searching, setSearching ] = useState( false );
40
41 const toastMessage = createInterpolateElement(
42 __(
43 'Included with Neve Business. <a>See plans</a>',
44 'templates-patterns-collection'
45 ),
46 {
47 a: (
48 <a
49 href={ tiobDash.onboardingUpsell.upgradeToast }
50 target="_blank"
51 rel="noopener noreferrer"
52 />
53 ),
54 }
55 );
56
57 useEffect( () => {
58 if ( showToast === 'dismissed' ) {
59 return;
60 }
61
62 const timeoutId = setTimeout( () => {
63 setShowToast( true );
64 }, 1000 );
65
66 if ( trackingId || trackingInitStarted ) {
67 return () => clearTimeout( timeoutId );
68 }
69
70 trackingInitStarted = true;
71 track( '', {
72 slug: 'neve',
73 license_id: tiobDash.license,
74 site: tiobDash.onboarding.homeUrl || '',
75 } )
76 .then( ( id ) => {
77 if ( id ) {
78 setTrackingId( id );
79 }
80 } )
81 .catch( () => {
82 trackingInitStarted = false;
83 } );
84
85 return () => clearTimeout( timeoutId );
86 }, [ showToast, trackingId, setShowToast, setTrackingId ] );
87
88 useEffect( () => {
89 if ( ! editor || ! onboarding?.root ) {
90 return undefined;
91 }
92
93 let active = true;
94 const reveal = setTimeout( () => {
95 if ( active ) {
96 setPersonalizing( true );
97 }
98 }, 300 );
99 const done = () => {
100 if ( active ) {
101 clearTimeout( reveal );
102 clearTimeout( safety );
103 setPersonalizing( false );
104 }
105 };
106
107 const safety = setTimeout( done, 9000 );
108 get(
109 onboarding.root +
110 '/starter_order?builder=' +
111 encodeURIComponent( editor )
112 )
113 .then( ( res ) => {
114 if ( active && res && Array.isArray( res.order ) && res.order.length ) {
115 setRankedOrder( editor, res.order );
116 }
117 done();
118 } )
119 .catch( done );
120
121 return () => {
122 active = false;
123 clearTimeout( reveal );
124 clearTimeout( safety );
125 setPersonalizing( false );
126 };
127 }, [ editor, setRankedOrder ] );
128
129 useEffect( () => {
130 if ( ! editor || ! onboarding?.root ) {
131 return undefined;
132 }
133
134 setSearchOrder( [] );
135 setSearchFailed( false );
136
137 const q = ( searchQuery || '' ).trim();
138 if ( q.length < SEARCH_MIN_CHARS ) {
139 setSearching( false );
140 return undefined;
141 }
142
143 let active = true;
144 let safety;
145 // Lets cleanup abort a superseded in-flight request, not just ignore its result.
146 const controller = new AbortController();
147 const done = () => {
148 if ( active ) {
149 clearTimeout( safety );
150 setSearching( false );
151 }
152 };
153 const fail = () => {
154 if ( active ) {
155 setSearchFailed( true );
156 }
157 done();
158 };
159
160 const timer = setTimeout( () => {
161 if ( ! active ) {
162 return;
163 }
164
165 setSearching( true );
166 safety = setTimeout( fail, 9000 );
167 get(
168 onboarding.root +
169 '/starter_search?builder=' +
170 encodeURIComponent( editor ) +
171 '&q=' +
172 encodeURIComponent( q ),
173 false,
174 true,
175 controller.signal
176 )
177 .then( ( res ) => {
178 if ( ! active ) {
179 return;
180 }
181
182 if ( res && Array.isArray( res.order ) && res.order.length ) {
183 setSearchOrder( res.order );
184 done();
185 return;
186 }
187
188 fail();
189 } )
190 .catch( ( err ) => {
191 // Aborted = superseded, not a real failure → skip the fallback.
192 if ( err && err.name === 'AbortError' ) {
193 return;
194 }
195 fail();
196 } );
197 }, SEARCH_DEBOUNCE_MS );
198
199 return () => {
200 active = false;
201 controller.abort();
202 clearTimeout( timer );
203 clearTimeout( safety );
204 };
205 }, [ searchQuery, editor, setSearchFailed, setSearchOrder ] );
206
207 return (
208 <div className="ob-container">
209 <div className="ob-container-inner">
210 <div className="ob-title-wrap">
211 <div className="ob-title-text">
212 <h1>
213 { __(
214 'Choose a design',
215 'templates-patterns-collection'
216 ) }
217 </h1>
218 <p className="ob-subtitle">
219 { createInterpolateElement(
220 __(
221 '<count>Nearly 200 starter sites</count> across every niche, with dozens added recently.',
222 'templates-patterns-collection'
223 ),
224 {
225 count: (
226 <span className="ob-subtitle__count" />
227 ),
228 }
229 ) }
230 </p>
231 </div>
232 <EditorSelector />
233 </div>
234 <Filters />
235 { ( personalizing || searching ) && (
236 <div
237 className="ob-ranking-loader"
238 role="status"
239 aria-live="polite"
240 >
241 <span className="ob-ranking-loader__dots">
242 <span />
243 <span />
244 <span />
245 </span>
246 <span className="ob-ranking-loader__text">
247 { searching || ( searchQuery || '' ).trim().length >= 3
248 ? __(
249 'Showing your matches — personalizing the rest in real time…',
250 'templates-patterns-collection'
251 )
252 : __(
253 'Personalizing your starter sites…',
254 'templates-patterns-collection'
255 ) }
256 </span>
257 </div>
258 ) }
259 <Sites />
260 { ! tiobDash.isValidLicense && (
261 <Toast
262 setShowToast={ setShowToast }
263 svgIcon={ SVG.logo }
264 className={ showToast === true ? 'show' : '' }
265 heading={ __(
266 'Unlock every premium template',
267 'templates-patterns-collection'
268 ) }
269 message={ toastMessage }
270 />
271 ) }
272 </div>
273 </div>
274 );
275 };
276
277 export default compose(
278 withSelect( ( select ) => ( {
279 editor: select( 'ti-onboarding' ).getCurrentEditor(),
280 searchQuery: select( 'ti-onboarding' ).getSearchQuery(),
281 trackingId: select( 'ti-onboarding' ).getTrackingId(),
282 } ) ),
283 withDispatch( ( dispatch ) => {
284 const {
285 setTrackingId,
286 setRankedOrder,
287 setSearchOrder,
288 setSearchFailed,
289 } = dispatch( 'ti-onboarding' );
290
291 return {
292 setTrackingId,
293 setRankedOrder,
294 setSearchOrder,
295 setSearchFailed,
296 };
297 } )
298 )( SiteList );
299