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 / StarterSiteCard.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 2 days 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 2 days ago Toast.js 2 weeks ago WelcomeMock.js 2 years ago
StarterSiteCard.js
343 lines
1 /* global tiobDash */
2 import { compose } from '@wordpress/compose';
3 import { withDispatch, withSelect } from '@wordpress/data';
4 import { useEffect, useMemo, useState } from '@wordpress/element';
5 import { __, sprintf } from '@wordpress/i18n';
6 import { track } from '../utils/rest';
7 import { decodeHtmlEntities } from '../utils/common';
8
9 const hashColor = ( value = '' ) => {
10 let hash = 0;
11 for ( let index = 0; index < value.length; index++ ) {
12 hash = value.charCodeAt( index ) + ( ( hash * 32 ) - hash );
13 }
14
15 return Math.abs( hash );
16 };
17
18 const swatchBackground = ( colorSlug = '' ) => {
19 const normalized = String( colorSlug ).trim().toLowerCase();
20 if ( ! normalized ) {
21 return 'linear-gradient(135deg, #cbd5e1 0%, #64748b 100%)';
22 }
23
24 if (
25 /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test( normalized ) ||
26 /^(rgb|rgba|hsl|hsla)\(/i.test( normalized )
27 ) {
28 return normalized;
29 }
30
31 if (
32 typeof window !== 'undefined' &&
33 window.CSS &&
34 typeof window.CSS.supports === 'function' &&
35 window.CSS.supports( 'color', normalized )
36 ) {
37 return normalized;
38 }
39
40 const hue = hashColor( normalized ) % 360;
41 const hueShift = ( hue + 24 ) % 360;
42
43 return `linear-gradient(135deg, hsl(${ hue } 72% 72%) 0%, hsl(${ hueShift } 72% 44%) 100%)`;
44 };
45
46 const StarterSiteCard = ( {
47 data,
48 setSite,
49 handleNextStep,
50 trackingId,
51 editor,
52 selectedColors = []
53 } ) => {
54 const {
55 upsell,
56 screenshot,
57 title,
58 category,
59 query,
60 site_description: siteDescription,
61 description,
62 } = data;
63 const cardDescription = ( siteDescription || description || '' ).trim();
64 const pageShots = useMemo( () => {
65 if ( ! Array.isArray( data?.page_shots ) || ! data.page_shots.length ) {
66 return screenshot
67 ? [
68 {
69 key: 'home',
70 label: __( 'Home', 'templates-patterns-collection' ),
71 screenshot,
72 },
73 ]
74 : [];
75 }
76
77 return data.page_shots
78 .filter( ( shot ) => shot && shot.screenshot )
79 .map( ( shot ) => ( {
80 key: shot.key || shot.label || 'page',
81 label: decodeHtmlEntities( shot.label || shot.key || __( 'Page', 'templates-patterns-collection' ) ),
82 screenshot: shot.screenshot,
83 } ) );
84 }, [ data?.page_shots, screenshot ] );
85 const colorOptions = useMemo( () => {
86 const colors = Array.isArray( data?.colors ) ? data.colors.filter( Boolean ) : [];
87 const screenshotMap =
88 data?.screenshots_by_color && typeof data.screenshots_by_color === 'object'
89 ? data.screenshots_by_color
90 : {};
91
92 return colors.map( ( color ) => ( {
93 slug: color,
94 label: color.replace( /[-_]/g, ' ' ),
95 screenshot: screenshotMap[ color ] || screenshot,
96 } ) );
97 }, [ data?.colors, data?.screenshots_by_color, screenshot ] );
98 // First globally-selected color this card actually has, returned as the
99 // canonical RAW colorOptions slug so option.slug === activeColor still hits.
100 // selectedColors are normalized lowercase (Filters.js) while colorOptions
101 // slugs / screenshots_by_color keys are raw, so match case-insensitively.
102 const filterColor = useMemo( () => {
103 if (
104 ! Array.isArray( selectedColors ) ||
105 ! selectedColors.length ||
106 ! colorOptions.length
107 ) {
108 return '';
109 }
110
111 const normalizedToSlug = new Map(
112 colorOptions.map( ( option ) => [
113 String( option.slug || '' ).trim().toLowerCase(),
114 option.slug,
115 ] )
116 );
117
118 const matched = selectedColors.find( ( color ) =>
119 normalizedToSlug.has( color )
120 );
121
122 return matched ? normalizedToSlug.get( matched ) : '';
123 }, [ selectedColors, colorOptions ] );
124 const previewColors = colorOptions.slice( 0, 2 );
125 const [ activePage, setActivePage ] = useState( pageShots[0]?.key || 'home' );
126 const [ activeColor, setActiveColor ] = useState( colorOptions[0]?.slug || '' );
127 const [ isColorExpanded, setIsColorExpanded ] = useState( false );
128
129 useEffect( () => {
130 setActivePage( pageShots[0]?.key || 'home' );
131 }, [ pageShots ] );
132
133 // Seed the displayed color from the global filter when this card matches a
134 // selected color; otherwise fall back to the first palette. Re-runs only on
135 // filter/colorOptions changes, so manual swatch clicks survive until then.
136 useEffect( () => {
137 setActiveColor( filterColor || colorOptions[0]?.slug || '' );
138 }, [ colorOptions, filterColor ] );
139
140 const activePageShot =
141 pageShots.find( ( shot ) => shot.key === activePage ) || pageShots[0] || null;
142 const activeColorOption =
143 colorOptions.find( ( option ) => option.slug === activeColor ) || colorOptions[0] || null;
144 const activeScreenshot =
145 activePage === 'home'
146 ? activeColorOption?.screenshot || activePageShot?.screenshot || screenshot
147 : activePageShot?.screenshot || activeColorOption?.screenshot || screenshot;
148
149 const launchPreview = () => {
150 setSite( {
151 ...data,
152 preview_screenshot: activeScreenshot,
153 preview_color: activeColor,
154 preview_page: activePage,
155 } );
156 handleNextStep();
157 const trackData = {
158 slug: 'neve',
159 license_id: tiobDash.license,
160 site: tiobDash.onboarding.homeUrl || '',
161 step_id: 2,
162 step_status: 'completed',
163 selected_template: title,
164 editor,
165 search: query,
166 cat: category,
167 };
168 track( trackingId, trackData ).catch( ( error ) => {
169 // eslint-disable-next-line no-console
170 console.error( error );
171 } );
172 };
173
174 return (
175 <div className="ss-card-wrap">
176 <div
177 className="ss-card"
178 role="button"
179 tabIndex={0}
180 onClick={(e) => {
181 e.preventDefault();
182 launchPreview();
183 }}
184 onKeyDown={(e) => {
185 if (e.key === 'Enter' || e.key === ' ') {
186 e.preventDefault();
187 launchPreview();
188 }
189 }}
190 >
191 { upsell && (
192 <span className="ss-badge">
193 <span>
194 { __( 'PRO', 'templates-patterns-collection' ) }
195 </span>
196 </span>
197 ) }
198
199 { activeScreenshot && (
200 <div
201 className="ss-image"
202 style={ {
203 backgroundImage: `url("${ activeScreenshot }")`,
204 } }
205 >
206 { pageShots.length > 1 && (
207 <div className="ss-page-shots">
208 { pageShots.map( ( shot ) => (
209 <button
210 type="button"
211 key={ shot.key }
212 className={
213 shot.key === activePage
214 ? 'ss-page-shot is-active'
215 : 'ss-page-shot'
216 }
217 onClick={ ( event ) => {
218 event.preventDefault();
219 event.stopPropagation();
220 setActivePage( shot.key );
221 } }
222 >
223 { shot.label }
224 </button>
225 ) ) }
226 </div>
227 ) }
228 { colorOptions.length > 1 && (
229 <div
230 className={
231 isColorExpanded
232 ? 'ss-color-summary is-expanded'
233 : 'ss-color-summary'
234 }
235 onMouseEnter={ () => setIsColorExpanded( true ) }
236 onMouseLeave={ () => setIsColorExpanded( false ) }
237 onFocus={ () => setIsColorExpanded( true ) }
238 onBlur={ ( event ) => {
239 if ( ! event.currentTarget.contains( event.relatedTarget ) ) {
240 setIsColorExpanded( false );
241 }
242 } }
243 >
244 { ! isColorExpanded ? (
245 <div className="ss-color-summary__collapsed">
246 <div className="ss-color-summary__swatches">
247 { previewColors.map( ( option ) => (
248 <span
249 key={ option.slug }
250 className="ss-color-dot"
251 style={ {
252 background: swatchBackground( option.slug ),
253 } }
254 aria-hidden="true"
255 title={ option.label }
256 />
257 ) ) }
258 </div>
259 { colorOptions.length > 2 && (
260 <span className="ss-color-summary__count">
261 +{ colorOptions.length - 2 }
262 </span>
263 ) }
264 </div>
265 ) : (
266 <div className="ss-color-summary__expanded">
267 { colorOptions.map( ( option ) => (
268 <button
269 type="button"
270 key={ option.slug }
271 className={
272 option.slug === activeColor
273 ? 'ss-color-swatch is-active'
274 : 'ss-color-swatch'
275 }
276 onClick={ ( event ) => {
277 event.preventDefault();
278 event.stopPropagation();
279 setActiveColor( option.slug );
280 } }
281 aria-label={ sprintf(
282 /* translators: %s is a color label. */
283 __(
284 'Preview %s color',
285 'templates-patterns-collection'
286 ),
287 option.label
288 ) }
289 title={ option.label }
290 >
291 <span
292 className="ss-color-dot"
293 style={ {
294 background: swatchBackground( option.slug ),
295 } }
296 aria-hidden="true"
297 />
298 </button>
299 ) ) }
300 </div>
301 ) }
302 </div>
303 ) }
304 </div>
305 ) }
306 </div>
307 <div className="ss-copy">
308 <p className="ss-title">{ title }</p>
309 { cardDescription && (
310 <p className="ss-description">{ cardDescription }</p>
311 ) }
312 </div>
313 </div>
314 );
315 };
316
317 export default compose(
318 withSelect( ( select ) => {
319 const {
320 getTrackingId,
321 getCurrentEditor,
322 getCurrentCategory,
323 getSearchQuery,
324 getSelectedColors,
325 } = select( 'ti-onboarding' );
326 return {
327 trackingId: getTrackingId(),
328 editor: getCurrentEditor(),
329 category: getCurrentCategory(),
330 query: getSearchQuery(),
331 selectedColors: getSelectedColors() || [],
332 };
333 } ),
334 withDispatch( ( dispatch, { data } ) => {
335 const { setCurrentSite, setOnboardingStep } =
336 dispatch( 'ti-onboarding' );
337 return {
338 setSite: ( nextSite = data ) => setCurrentSite( nextSite ),
339 handleNextStep: () => setOnboardingStep( 3 ),
340 };
341 } )
342 )( StarterSiteCard );
343