@@ -119,6 +119,185 @@ function preloadFile(href, as) {
119
119
document . head . appendChild ( link ) ;
120
120
}
121
121
122
+ function parseVariants ( ) {
123
+ const regex = / \b ( \d + ( \. \d { 1 , 2 } ) ? ) \b \s * ( [ A - Z ] { 3 } ) \b / ;
124
+ return Array . from ( document . querySelectorAll ( '.product-variants > div' ) ) . map ( ( variantRow ) => {
125
+ const columns = Array . from ( variantRow . querySelectorAll ( ':scope > div' ) ) . map ( ( col ) => col . textContent . trim ( ) ) ;
126
+
127
+ const regularPriceCol = columns [ 4 ] ;
128
+ const finalPriceCol = columns [ 5 ] ;
129
+ const regularPriceMatch = regularPriceCol . match ( regex ) ;
130
+ const finalPriceMatch = finalPriceCol . match ( regex ) ;
131
+
132
+ const variant = {
133
+ price : { roles : [ 'visible' ] } ,
134
+ } ;
135
+ if ( regularPriceMatch ) {
136
+ variant . price . regular = {
137
+ amount : {
138
+ currency : regularPriceMatch [ 3 ] ,
139
+ value : parseFloat ( regularPriceMatch [ 1 ] ) ,
140
+ } ,
141
+ } ;
142
+ }
143
+ if ( finalPriceMatch ) {
144
+ variant . price . final = {
145
+ amount : {
146
+ currency : finalPriceMatch [ 3 ] ,
147
+ value : parseFloat ( finalPriceMatch [ 1 ] ) ,
148
+ } ,
149
+ } ;
150
+ }
151
+
152
+ return variant ;
153
+ } ) ;
154
+ }
155
+
156
+ function computePriceRange ( variants ) {
157
+ const finalPriceValues = variants . map ( ( v ) => v . price . final . amount . value ) ;
158
+ const regularPriceValues = variants . map ( ( v ) => v . price . regular . amount . value ) ;
159
+
160
+ const minFinal = Math . min ( ...finalPriceValues ) ;
161
+ const maxFinal = Math . max ( ...finalPriceValues ) ;
162
+ const minRegular = Math . min ( ...regularPriceValues ) ;
163
+ const maxRegular = Math . max ( ...regularPriceValues ) ;
164
+ const { currency } = variants [ 0 ] . price . final . amount ;
165
+
166
+ return {
167
+ maximum : {
168
+ final : {
169
+ amount : {
170
+ value : maxFinal ,
171
+ currency,
172
+ } ,
173
+ } ,
174
+ regular : {
175
+ amount : {
176
+ value : maxRegular ,
177
+ currency,
178
+ } ,
179
+ } ,
180
+ roles : [ 'visible' ] ,
181
+ } ,
182
+ minimum : {
183
+ final : {
184
+ amount : {
185
+ value : minFinal ,
186
+ currency,
187
+ } ,
188
+ } ,
189
+ regular : {
190
+ amount : {
191
+ value : minRegular ,
192
+ currency,
193
+ } ,
194
+ } ,
195
+ roles : [ 'visible' ] ,
196
+ } ,
197
+ } ;
198
+ }
199
+
200
+ function parseProductData ( ) {
201
+ const name = document . querySelector ( 'h1' ) ?. textContent ?. trim ( ) ?? '' ;
202
+
203
+ const descriptionParagraphs = document . querySelectorAll ( 'main > div > p' ) ;
204
+ const description = Array . from ( descriptionParagraphs ) . map ( ( paragraph ) => paragraph . innerHTML ) . join ( '<br/>' ) ;
205
+ const hasVariants = document . querySelector ( '.product-variants' ) !== null ;
206
+
207
+ const attributes = Array . from ( document . querySelectorAll ( '.product-attributes > div' ) ) . map ( ( attributeRow ) => {
208
+ const cells = attributeRow . querySelectorAll ( ':scope > div' ) ;
209
+ const [ attributeName , attributeLabel , attributeValue ] = Array . from ( cells )
210
+ . map ( ( cell ) => cell . textContent . trim ( ) ) ;
211
+
212
+ // TODO: This should probably be a ul/li list to better split the values
213
+ let value = attributeValue . split ( ',' ) ;
214
+ value = value . length === 1 ? value [ 0 ] : value ;
215
+ return { name : attributeName , label : attributeLabel , value } ;
216
+ } ) ;
217
+
218
+ const images = Array . from ( document . querySelectorAll ( '.product-images img' ) ) . map ( ( img ) => {
219
+ const src = new URL ( img . getAttribute ( 'src' ) , window . location ) ;
220
+ // Clear query parameters
221
+ src . searchParams . delete ( 'width' ) ;
222
+ src . searchParams . delete ( 'format' ) ;
223
+ src . searchParams . delete ( 'optimize' ) ;
224
+ const alt = img . getAttribute ( 'alt' ) || '' ;
225
+ return { url : src . toString ( ) , label : alt , roles : [ ] } ;
226
+ } ) ;
227
+
228
+ const product = {
229
+ __typename : hasVariants ? 'ComplexProductView' : 'SimpleProductView' ,
230
+ id : '' ,
231
+ externalId : getMetadata ( 'externalid' ) ,
232
+ sku : getMetadata ( 'sku' ) . toUpperCase ( ) ,
233
+ name,
234
+ description,
235
+ shortDescription : '' ,
236
+ url : getMetadata ( 'og:url' ) ,
237
+ urlKey : getMetadata ( 'urlkey' ) ,
238
+ inStock : getMetadata ( 'instock' ) === 'true' ,
239
+ metaTitle : '' ,
240
+ metaKeyword : '' ,
241
+ metaDescription : '' ,
242
+ addToCartAllowed : getMetadata ( 'addtocartallowed' ) === 'true' ,
243
+ images,
244
+ attributes,
245
+ } ;
246
+
247
+ if ( hasVariants ) {
248
+ // Add options
249
+ const options = [ ] ;
250
+ Array . from ( document . querySelectorAll ( '.product-options > div' ) ) . forEach ( ( optionRow ) => {
251
+ const cells = Array . from ( optionRow . querySelectorAll ( ':scope > div' ) ) . map ( ( cell ) => cell . textContent . trim ( ) ) ;
252
+ if ( cells [ 0 ] . toLowerCase ( ) !== 'option' ) {
253
+ const [ id , title , typeName , type , multiple , required ] = cells ;
254
+ options . push ( {
255
+ id,
256
+ title,
257
+ typeName,
258
+ type,
259
+ multiple,
260
+ required,
261
+ values : [ ] ,
262
+ } ) ;
263
+ } else {
264
+ const [ , valueId , valueTitle , value , selected , valueInStock ] = cells ;
265
+ if ( valueId && options . length > 0 ) {
266
+ options [ options . length - 1 ] . values . push ( {
267
+ id : valueId ,
268
+ title : valueTitle ,
269
+ value : value ?? valueTitle ,
270
+ type : 'TEXT' , // TODO
271
+ selected,
272
+ inStock : valueInStock ,
273
+ } ) ;
274
+ }
275
+ }
276
+ } ) ;
277
+ product . options = options ;
278
+ }
279
+
280
+ if ( ! hasVariants ) {
281
+ const priceValue = parseInt ( getMetadata ( 'product:price-amount' ) , 10 ) ;
282
+ let price = { } ;
283
+ if ( ! Number . isNaN ( priceValue ) ) {
284
+ const currency = getMetadata ( 'product:price-currency' ) || 'USD' ;
285
+ price = {
286
+ roles : [ 'visible' ] ,
287
+ regular : { value : priceValue , currency } ,
288
+ final : { value : priceValue , currency } ,
289
+ } ;
290
+ }
291
+ product . price = price ;
292
+ } else {
293
+ // Get all variant prices
294
+ const variants = parseVariants ( ) ;
295
+ product . priceRange = computePriceRange ( variants ) ;
296
+ }
297
+
298
+ return product ;
299
+ }
300
+
122
301
/**
123
302
* Loads everything needed to get to LCP.
124
303
* @param {Element } doc The container element
@@ -140,7 +319,32 @@ async function loadEager(doc) {
140
319
window . adobeDataLayer = window . adobeDataLayer || [ ] ;
141
320
142
321
let pageType = 'CMS' ;
143
- if ( document . body . querySelector ( 'main .product-details' ) ) {
322
+
323
+ // TODO: Parse content
324
+ const ogType = getMetadata ( 'og:type' ) ;
325
+ const skuFromMetadata = getMetadata ( 'sku' ) ;
326
+
327
+ if ( ogType === 'product' && skuFromMetadata ) {
328
+ pageType = 'Product' ;
329
+
330
+ window . getProductPromise = Promise . resolve ( parseProductData ( ) ) ;
331
+ const main = document . querySelector ( 'main' ) ;
332
+
333
+ // Remove all other blocks
334
+ main . textContent = '' ;
335
+
336
+ // Create product-details block
337
+ const wrapper = document . createElement ( 'div' ) ;
338
+ const block = buildBlock ( 'product-details' , '' ) ;
339
+ wrapper . append ( block ) ;
340
+ main . append ( wrapper ) ;
341
+
342
+ preloadFile ( '/scripts/__dropins__/storefront-pdp/containers/ProductDetails.js' , 'script' ) ;
343
+ preloadFile ( '/scripts/__dropins__/storefront-pdp/api.js' , 'script' ) ;
344
+ preloadFile ( '/scripts/__dropins__/storefront-pdp/render.js' , 'script' ) ;
345
+ preloadFile ( '/scripts/__dropins__/storefront-pdp/chunks/initialize.js' , 'script' ) ;
346
+ preloadFile ( '/scripts/__dropins__/storefront-pdp/chunks/getRefinedProduct.js' , 'script' ) ;
347
+ } else if ( document . body . querySelector ( 'main .product-details' ) ) {
144
348
pageType = 'Product' ;
145
349
const sku = getSkuFromUrl ( ) ;
146
350
window . getProductPromise = getProduct ( sku ) ;
0 commit comments