PluginProbe ʕ •ᴥ•ʔ
Starter Sites & Templates by Neve / 1.4.1
Starter Sites & Templates by Neve v1.4.1
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 / Sites.js
templates-patterns-collection / onboarding / src / Components Last commit date
CustomizeControls 2 months ago Steps 2 weeks ago App.js 2 years ago CategoryButtons.js 3 weeks ago CustomTooltip.js 2 years ago EditorSelector.js 2 years ago FeaturesList.js 1 day ago Filters.js 3 weeks ago Header.js 3 weeks ago ImportError.js 2 years ago ImportForm.js 1 year ago ImportMock.js 2 years ago ImportProgress.js 1 year ago Onboarding.js 3 weeks ago ProgressBar.js 2 years ago Search.js 3 weeks ago SitePreview.js 2 years ago SiteSettings.js 2 months ago Sites.js 2 weeks ago StarterSiteCard.js 1 day ago Toast.js 2 weeks ago WelcomeMock.js 2 years ago
Sites.js
339 lines
1 import { Fragment, useEffect, useMemo, useState } from '@wordpress/element';
2 import { withSelect } from '@wordpress/data';
3 import { __ } from '@wordpress/i18n';
4 import StarterSiteCard from './StarterSiteCard';
5 import VizSensor from 'react-visibility-sensor';
6 import { matchesCategory, searchCatalog } from '../utils/search';
7
8 /**
9 * @typedef {Object} Site
10 * @property {string} url
11 * @property {string} remote_url
12 * @property {string} screenshot
13 * @property {string} title
14 * @property {string[]} keywords
15 * @property {boolean} isNew
16 * @property {string} slug
17 */
18
19 const Sites = ( {
20 getSites,
21 editor,
22 category,
23 searchQuery,
24 rankedOrder,
25 searchOrder,
26 searchFailed,
27 sortBy,
28 selectedColors,
29 } ) => {
30 const [ maxShown, setMaxShown ] = useState( 12 );
31 const { sites = {} } = getSites;
32
33 useEffect( () => {
34 setMaxShown( 12 );
35 }, [ editor, category, searchQuery, sortBy, selectedColors ] );
36
37 const getBuilders = () => Object.keys( sites );
38
39 const getAllSites = () => {
40 const finalData = {};
41 const builders = getBuilders();
42
43 builders.forEach( ( builder ) => {
44 const sitesData = sites && sites[ builder ] ? sites[ builder ] : {};
45 finalData[ builder ] = Object.entries( sitesData ).map(
46 ( [ slug, site ] ) => ( { ...site, slug } )
47 );
48 } );
49
50 return finalData;
51 };
52
53 const pickBySlugs = ( siteList, slugs ) => {
54 if ( ! Array.isArray( siteList ) || ! Array.isArray( slugs ) ) {
55 return { picked: [], placed: {} };
56 }
57
58 const bySlug = new Map(
59 siteList
60 .filter( ( site ) => site && site.slug )
61 .map( ( site ) => [ site.slug, site ] )
62 );
63 const picked = [];
64 const placed = {};
65
66 slugs.forEach( ( slug ) => {
67 if ( ! slug || placed[ slug ] || ! bySlug.has( slug ) ) {
68 return;
69 }
70
71 placed[ slug ] = true;
72 picked.push( bySlug.get( slug ) );
73 } );
74
75 return { picked, placed };
76 };
77
78 const applyRanking = ( sitesToRank, order ) => {
79 if ( ! Array.isArray( order ) || ! order.length ) {
80 return sitesToRank;
81 }
82
83 const { picked, placed } = pickBySlugs( sitesToRank, order );
84 sitesToRank.forEach( ( site ) => {
85 if ( site && ( ! site.slug || ! placed[ site.slug ] ) ) {
86 picked.push( site );
87 }
88 } );
89
90 return picked;
91 };
92
93 const applySort = ( list, mode, fullList ) => {
94 const rowIndex = new Map(
95 fullList.map( ( site, index ) => [ site.slug, index ] )
96 );
97 const byRow = ( a, b ) =>
98 ( rowIndex.get( a.slug ) ?? Number.MAX_SAFE_INTEGER ) -
99 ( rowIndex.get( b.slug ) ?? Number.MAX_SAFE_INTEGER );
100
101 if ( mode === 'new' ) {
102 return [ ...list ].sort(
103 ( a, b ) =>
104 ( b.isNew ? 1 : 0 ) - ( a.isNew ? 1 : 0 ) || byRow( a, b )
105 );
106 }
107
108 return [ ...list ].sort( byRow );
109 };
110
111 const sortByKeywords = ( keyword, sitesToSort ) => {
112 const normalizedKeyword = keyword?.toLowerCase();
113
114 return sitesToSort.sort( ( a, b ) => {
115 const aHasKeywords = Array.isArray( a?.keywords );
116 const bHasKeywords = Array.isArray( b?.keywords );
117
118 if ( ! aHasKeywords && ! bHasKeywords ) {
119 return 0;
120 }
121 if ( ! aHasKeywords ) {
122 return 1;
123 }
124 if ( ! bHasKeywords ) {
125 return -1;
126 }
127
128 const aHasKeyword = a.keywords.includes( normalizedKeyword );
129 const bHasKeyword = b.keywords.includes( normalizedKeyword );
130
131 if ( aHasKeyword && ! bHasKeyword ) {
132 return -1;
133 }
134 if ( ! aHasKeyword && bHasKeyword ) {
135 return 1;
136 }
137
138 const aIndex = a.keywords.findIndex( ( value ) => value === normalizedKeyword );
139 const bIndex = b.keywords.findIndex( ( value ) => value === normalizedKeyword );
140
141 return aIndex - bIndex;
142 } );
143 };
144
145 const filterBySearch = ( items ) => {
146 if ( ! searchQuery ) {
147 return items;
148 }
149
150 if ( Array.isArray( searchOrder ) && searchOrder.length ) {
151 return pickBySlugs( items, searchOrder ).picked;
152 }
153
154 if ( searchFailed ) {
155 return searchCatalog( items, searchQuery );
156 }
157
158 return [];
159 };
160
161 const filterByCategory = ( items, cat ) => {
162 if ( 'free' === cat ) {
163 return items.filter( ( item ) => matchesCategory( item, cat ) );
164 }
165
166 if ( cat && 'all' !== cat ) {
167 const filteredByCat = items.filter( ( item ) =>
168 matchesCategory( item, cat )
169 );
170
171 return sortByKeywords( cat, filteredByCat );
172 }
173
174 return items;
175 };
176
177 const filterByColors = ( items ) => {
178 if ( ! Array.isArray( selectedColors ) || ! selectedColors.length ) {
179 return items;
180 }
181
182 return items.filter( ( site ) => {
183 if ( ! Array.isArray( site?.colors ) || ! site.colors.length ) {
184 return false;
185 }
186
187 const siteColors = site.colors.map( ( color ) =>
188 String( color || '' ).trim().toLowerCase()
189 );
190
191 return selectedColors.some( ( color ) => siteColors.includes( color ) );
192 } );
193 };
194
195 const getFilteredSites = () => {
196 const allSites = getAllSites();
197 if ( Object.keys( allSites ).length === 0 ) {
198 return { list: [], matchCount: 0 };
199 }
200
201 const fullBucket = allSites[ editor ] || [];
202 let ranked = fullBucket;
203 if ( searchQuery || ! sortBy || sortBy === 'recommended' ) {
204 ranked = applyRanking( fullBucket, rankedOrder && rankedOrder[ editor ] );
205 }
206
207 if ( searchQuery ) {
208 const matches = filterByColors(
209 filterByCategory( filterBySearch( ranked ), category )
210 );
211 const inMatches = {};
212 matches.forEach( ( site ) => {
213 if ( site && site.slug ) {
214 inMatches[ site.slug ] = true;
215 }
216 } );
217
218 const rest = filterByColors( filterByCategory( ranked, category ) ).filter(
219 ( site ) => site && site.slug && ! inMatches[ site.slug ]
220 );
221 const remainder = matches.length % 4;
222 const pad =
223 remainder === 0 ? 0 : Math.min( 4 - remainder, rest.length );
224
225 return {
226 list: [ ...matches, ...rest ],
227 matchCount: matches.length + pad,
228 };
229 }
230
231 let builderSites = filterByColors( filterByCategory( ranked, category ) );
232 if ( sortBy === 'popular' || sortBy === 'new' ) {
233 builderSites = applySort( builderSites, sortBy, fullBucket );
234 }
235
236 return { list: builderSites, matchCount: 0 };
237 };
238
239 const { list: allData, matchCount } = useMemo(
240 () => getFilteredSites(),
241 // eslint-disable-next-line react-hooks/exhaustive-deps
242 [
243 sites,
244 editor,
245 category,
246 searchQuery,
247 sortBy,
248 selectedColors,
249 rankedOrder,
250 searchOrder,
251 searchFailed,
252 ]
253 );
254
255 return (
256 <>
257 { allData.length ? (
258 <div className="ob-sites is-grid">
259 { allData.slice( 0, maxShown ).map( ( site, index ) => (
260 <Fragment key={ site.slug || index }>
261 { searchQuery &&
262 matchCount > 0 &&
263 index === matchCount && (
264 <div className="ob-results-divider">
265 <span>
266 { __(
267 'Not quite right? Browse more',
268 'templates-patterns-collection'
269 ) }
270 </span>
271 </div>
272 ) }
273 <StarterSiteCard data={ site } />
274 </Fragment>
275 ) ) }
276
277 <VizSensor
278 onChange={ ( isVisible ) => {
279 if ( ! isVisible ) {
280 return false;
281 }
282
283 setMaxShown( ( shown ) => shown + 12 );
284 } }
285 >
286 <span
287 style={ {
288 height: 10,
289 width: 10,
290 display: 'block',
291 } }
292 />
293 </VizSensor>
294 </div>
295 ) : searchQuery && ! searchFailed && ! searchOrder.length ? null : (
296 <div className="ob-no-results">
297 <p>
298 { __(
299 'No results found.',
300 'templates-patterns-collection'
301 ) }
302 &nbsp;
303 { __(
304 'You can try a different search or use another category.',
305 'templates-patterns-collection'
306 ) }
307 </p>
308 </div>
309 ) }
310 </>
311 );
312 };
313
314 export default withSelect( ( select ) => {
315 const {
316 getCurrentEditor,
317 getCurrentCategory,
318 getSites,
319 getSearchQuery,
320 getRankedOrder,
321 getSearchOrder,
322 getSearchFailed,
323 getSortBy,
324 getSelectedColors,
325 } = select( 'ti-onboarding' );
326
327 return {
328 editor: getCurrentEditor(),
329 category: getCurrentCategory(),
330 searchQuery: getSearchQuery(),
331 getSites: getSites(),
332 rankedOrder: getRankedOrder(),
333 searchOrder: getSearchOrder(),
334 searchFailed: getSearchFailed(),
335 sortBy: getSortBy(),
336 selectedColors: getSelectedColors(),
337 };
338 } )( Sites );
339