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 |