Skip to content

Commit 384ca42

Browse files
committed
Parse product data from document
1 parent f8e0fcd commit 384ca42

File tree

2 files changed

+206
-2
lines changed

2 files changed

+206
-2
lines changed

blocks/product-details/product-details.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ export default async function decorate(block) {
181181
setTimeout(async () => {
182182
try {
183183
await productRenderer.render(ProductDetails, {
184-
sku: getSkuFromUrl(),
184+
sku: product.sku ?? getSkuFromUrl(),
185185
carousel: {
186186
controls: {
187187
desktop: 'thumbnailsColumn',

scripts/scripts.js

+205-1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,185 @@ function preloadFile(href, as) {
119119
document.head.appendChild(link);
120120
}
121121

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+
122301
/**
123302
* Loads everything needed to get to LCP.
124303
* @param {Element} doc The container element
@@ -140,7 +319,32 @@ async function loadEager(doc) {
140319
window.adobeDataLayer = window.adobeDataLayer || [];
141320

142321
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')) {
144348
pageType = 'Product';
145349
const sku = getSkuFromUrl();
146350
window.getProductPromise = getProduct(sku);

0 commit comments

Comments
 (0)