Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions components/Image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,26 @@ const imageKit = new ImageKit({

const FACTORS = [1, 1.5, 2];

export const getOptimizedMediaUrl = (
{ originalSrc, width, height, factor }: {
originalSrc: string;
width: number;
height?: number;
factor: number;
},
) =>
imageKit.url({
path: originalSrc,
transformation: [{
width: `${Math.trunc(factor * width)}`,
height: height ? `${Math.trunc(factor * height)}` : undefined,
}],
});

export const getSrcSet = (src: string, width: number, height?: number) =>
FACTORS
.map((factor) =>
`${
imageKit.url({
path: src,
transformation: [{
width: `${Math.trunc(factor * width)}`,
height: height ? `${Math.trunc(factor * height)}` : undefined,
}],
})
`${getOptimizedMediaUrl({ originalSrc: src, width, height, factor })})
} ${Math.trunc(factor * width)}w`
)
.join(", ");
Expand Down
163 changes: 163 additions & 0 deletions components/VTEXPortalDataLayerCompatibility.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { Product } from "deco-sites/std/commerce/types.ts";

// deno-lint-ignore no-explicit-any
function addVTEXPortalDataSnippet(accountName: any) {
const url = new URL(window.location.href);
const structuredDataScripts =
document.querySelectorAll('script[type="application/ld+json"]') || [];
// deno-lint-ignore no-explicit-any
const structuredDatas: Record<string, any>[] = [];
// deno-lint-ignore no-explicit-any
structuredDataScripts.forEach((v: any) => {
structuredDatas.push(JSON.parse(v.text));
});
const isProductPage = structuredDatas.some((s) => s["@type"] === "Product");

// deno-lint-ignore no-explicit-any
const props: Record<string, any> = {
pageCategory: "Home",
pageDepartment: null,
pageUrl: window.location.href,
pageTitle: document.title,
skuStockOutFromShelf: [],
skuStockOutFromProductDetail: [],
accountName: `${accountName}`,
pageFacets: [],
shelfProductIds: [],
};

if (isProductPage) {
props.pageCategory = "Product";
const scriptEl: HTMLScriptElement | null = document.querySelector(
'script[data-id="vtex-portal-compat"]',
);
if (scriptEl) {
Object.assign(props, JSON.parse(scriptEl.dataset.datalayer || "{}"));
}
}

const breadcrumbSD = structuredDatas.find((
s,
) => (s["@type"] === "BreadcrumbList"));
if (breadcrumbSD) {
const department = breadcrumbSD?.itemListElement?.[0];
props.pageDepartment = department?.name || null;
if (props.pageDepartment) {
!isProductPage && (props.pageCategory = "Category");
const category = breadcrumbSD?.itemListElement
?.[breadcrumbSD?.itemListElement.length - 1];
// TODO: Corrigir na pDP
props.categoryName = category?.name;
} else {
props.pageCategory = new URL(window.location.href).pathname.split("/")
.filter(Boolean).join(" ");
}
}

document.querySelectorAll("[data-product-id]").forEach(
(el) => {
props.shelfProductIds.push((el as HTMLDivElement).dataset.productId);
},
);

window.dataLayer = window.dataLayer || [];
window.dataLayer.unshift(props);

if (url.pathname === "/") {
window.dataLayer.push({ event: "homeView" });
} else if (props.pageCategory === "Product") {
window.dataLayer.push({ event: "productView" });
} else if (props.pageCategory === "Category") {
window.dataLayer.push({ event: "categoryView" });
} else {
window.dataLayer.push({ event: "otherView" });
}
}

export function AddVTEXPortalData({ accountName }: { accountName: string }) {
return (
<script
id="datalayer-portal-compat"
dangerouslySetInnerHTML={{
__html: `(${addVTEXPortalDataSnippet.toString()})('${accountName}')`,
}}
/>
);
}

export function ProductDetailsTemplate({ product }: { product: Product }) {
const departament = product.additionalProperty?.find((p) =>
p.name === "category"
);
const category = product.additionalProperty?.findLast((p) =>
p.name === "category"
);

const offers = product.offers?.offers;
const lowestOffer = offers?.[0]?.priceSpecification;
const highestOffer = offers?.[offers.length - 1]?.priceSpecification;
const template = {
productId: product.isVariantOf?.productGroupID,
productReferenceId: product.isVariantOf?.model,
productEans: product.isVariantOf?.hasVariant.map((s) => s.gtin) || [],
skuStock: product.isVariantOf?.hasVariant.reduce((result, sku) => {
if (sku.offers?.offers?.[0]?.inventoryLevel.value) {
// @ts-expect-error nao faz sentido
result[sku.id!] = sku.offers?.offers?.[0]?.inventoryLevel.value;
}
return result;
}, {} as Record<string, number>),
productName: product.isVariantOf?.name,
brand: product.brand,
brandId: product.brand,
productDepartmentId: departament?.propertyID,
productDepartmentName: departament?.value,
productCategoryId: category?.propertyID,
productCategoryName: category?.value,
productListPriceFrom: lowestOffer?.[0]?.price,
productListPriceTo: highestOffer?.[0]?.price,
productPriceFrom: lowestOffer?.[1]?.price,
productPriceTo: highestOffer?.[1]?.price,
sellerId: offers?.map(({ seller }) => seller)?.[0],
sellerIds: offers?.map(({ seller }) => seller),
};

return (
<script
data-id="vtex-portal-compat"
data-datalayer={JSON.stringify(template)}
/>
);
}

interface ProductShelfIdsProps {
product: Product;
}
export function ProductCardId({ product }: ProductShelfIdsProps) {
if (!product.isVariantOf?.productGroupID) return null;
return (
<div
data-product-id={product.isVariantOf?.productGroupID}
style={{ display: "none" }}
/>
);
}

export interface ProductSKUJsonProps {
product: unknown;
}
export function ProductSKUJson({ product }: ProductSKUJsonProps) {
return (
<script
dangerouslySetInnerHTML={{
__html: `var skuJson = ${JSON.stringify(product)}`,
}}
/>
);
}

// How to use:
// 1. add the AddVTEXPortalData at routes/_app.tsx after <props.Component />
// 2. add the ProductDetailsTemplate at ProductDetails.tsx for routes /:slug/p
// 3. add ProductShelfIds at product shelves
// 4. Add VTEXPortalDataLayerCompatibility section to PDP
11 changes: 9 additions & 2 deletions components/Video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { forwardRef } from "preact/compat";
import type { JSX } from "preact";

import { getSrcSet } from "./Image.tsx";
import { getOptimizedMediaUrl, getSrcSet } from "./Image.tsx";

type Props =
& Omit<JSX.IntrinsicElements["video"], "width" | "height" | "preload">
Expand All @@ -19,11 +19,18 @@ const Video = forwardRef<HTMLVideoElement, Props>((props, ref) => {
const { loading = "lazy" } = props;
const srcSet = getSrcSet(props.src, props.width, props.height);

const optimizedVideoSrc = getOptimizedMediaUrl({
originalSrc: props.src,
width: props.width,
height: props.height,
factor: 1,
});

return (
<video
{...props}
preload={undefined}
src={props.src}
src={optimizedVideoSrc}
srcSet={srcSet}
loading={loading}
ref={ref}
Expand Down
10 changes: 6 additions & 4 deletions live.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ import * as $$$$$$$$4 from "./sections/configOCC.global.tsx";
import * as $$$$$$$$5 from "./sections/Analytics.tsx";
import * as $$$$$$$$6 from "./sections/configShopify.global.tsx";
import * as $$$$$$$$7 from "./sections/configVNDA.global.tsx";
import * as $$$$$$$$8 from "./sections/configVTEX.global.tsx";
import * as $$$$$$$$9 from "./sections/SEOPDP.tsx";
import * as $$$$$$$$8 from "./sections/VTEXPortalDataLayerCompatibility.tsx";
import * as $$$$$$$$9 from "./sections/configVTEX.global.tsx";
import * as $$$$$$$$10 from "./sections/SEOPDP.tsx";
import * as $$$$$$$$$$$0 from "./actions/vtex/notifyme.ts";
import * as $$$$$$$$$$$1 from "./actions/vtex/cart/updateCoupons.ts";
import * as $$$$$$$$$$$2 from "./actions/vtex/cart/updateAttachment.ts";
Expand Down Expand Up @@ -191,11 +192,12 @@ const manifest = {
"deco-sites/std/sections/configOCC.global.tsx": $$$$$$$$4,
"deco-sites/std/sections/configShopify.global.tsx": $$$$$$$$6,
"deco-sites/std/sections/configVNDA.global.tsx": $$$$$$$$7,
"deco-sites/std/sections/configVTEX.global.tsx": $$$$$$$$8,
"deco-sites/std/sections/configVTEX.global.tsx": $$$$$$$$9,
"deco-sites/std/sections/configYourViews.global.tsx": $$$$$$$$0,
"deco-sites/std/sections/SEO.tsx": $$$$$$$$2,
"deco-sites/std/sections/SEOPDP.tsx": $$$$$$$$9,
"deco-sites/std/sections/SEOPDP.tsx": $$$$$$$$10,
"deco-sites/std/sections/SEOPLP.tsx": $$$$$$$$3,
"deco-sites/std/sections/VTEXPortalDataLayerCompatibility.tsx": $$$$$$$$8,
},
"actions": {
"$live/actions/workflows/cancel.ts": i1$$$$$$$0,
Expand Down
40 changes: 39 additions & 1 deletion schemas.gen.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"deco-sites/std/sections/SEO.tsx",
"deco-sites/std/sections/SEOPDP.tsx",
"deco-sites/std/sections/SEOPLP.tsx",
"deco-sites/std/sections/VTEXPortalDataLayerCompatibility.tsx",
"$live/matchers/MatchAlways.ts",
"$live/matchers/MatchDate.ts",
"$live/matchers/MatchEnvironment.ts",
Expand Down Expand Up @@ -7063,6 +7064,19 @@
],
"title": "deco-sites/std/components/seo/SEOPLP.tsx@Props"
},
"ZGVjby1zaXRlcy9zdGQvc2VjdGlvbnMvVlRFWFBvcnRhbERhdGFMYXllckNvbXBhdGliaWxpdHkudHN4@LoaderProps": {
"type": "object",
"properties": {
"slug": {
"$ref": "#/definitions/ZGVjby1zaXRlcy9zdGQvZnVuY3Rpb25zL3JlcXVlc3RUb1BhcmFtLnRz@RequestURLParam",
"title": "Slug"
}
},
"required": [
"slug"
],
"title": "deco-sites/std/sections/VTEXPortalDataLayerCompatibility.tsx@LoaderProps"
},
"ZGVjby1jeC9saXZlL21hdGNoZXJzL01hdGNoRGF0ZS50cw==@Props": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -9226,6 +9240,27 @@
}
}
},
"ZGVjby1zaXRlcy9zdGQvc2VjdGlvbnMvVlRFWFBvcnRhbERhdGFMYXllckNvbXBhdGliaWxpdHkudHN4": {
"title": "deco-sites/std/sections/VTEXPortalDataLayerCompatibility.tsx",
"type": "object",
"allOf": [
{
"$ref": "#/definitions/ZGVjby1zaXRlcy9zdGQvc2VjdGlvbnMvVlRFWFBvcnRhbERhdGFMYXllckNvbXBhdGliaWxpdHkudHN4@LoaderProps"
}
],
"required": [
"__resolveType"
],
"properties": {
"__resolveType": {
"type": "string",
"enum": [
"deco-sites/std/sections/VTEXPortalDataLayerCompatibility.tsx"
],
"default": "deco-sites/std/sections/VTEXPortalDataLayerCompatibility.tsx"
}
}
},
"JGxpdmUvbWF0Y2hlcnMvTWF0Y2hBbHdheXMudHM=": {
"title": "$live/matchers/MatchAlways.ts",
"type": "object",
Expand Down Expand Up @@ -10132,6 +10167,9 @@
},
{
"$ref": "#/definitions/ZGVjby1zaXRlcy9zdGQvc2VjdGlvbnMvU0VPUExQLnRzeA=="
},
{
"$ref": "#/definitions/ZGVjby1zaXRlcy9zdGQvc2VjdGlvbnMvVlRFWFBvcnRhbERhdGFMYXllckNvbXBhdGliaWxpdHkudHN4"
}
]
},
Expand Down Expand Up @@ -10332,4 +10370,4 @@
}
}
}
}
}
38 changes: 38 additions & 0 deletions sections/VTEXPortalDataLayerCompatibility.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
ProductSKUJson,
ProductSKUJsonProps as Props,
} from "deco-sites/std/components/VTEXPortalDataLayerCompatibility.tsx";
import type { RequestURLParam } from "deco-sites/std/functions/requestToParam.ts";
import { paths } from "deco-sites/std/packs/vtex/utils/paths.ts";
import type { Context } from "deco-sites/std/packs/vtex/accounts/vtex.ts";
import { getSegment } from "deco-sites/std/packs/vtex/utils/segment.ts";
import { toSegmentParams } from "deco-sites/std/packs/vtex/utils/legacy.ts";
import { fetchAPI } from "deco-sites/std/utils/fetchVTEX.ts";
import type { LegacyProduct } from "deco-sites/std/packs/vtex/types.ts";
import { withSegmentCookie } from "deco-sites/std/packs/vtex/utils/segment.ts";

export default function VTEXPortalDataLayerCompatibility({ product }: Props) {
return <ProductSKUJson product={product} />;
}

export interface LoaderProps {
slug: RequestURLParam;
}

export async function loader(props: LoaderProps, req: Request, ctx: Context) {
const { configVTEX: config } = ctx;
const { slug } = props;
const segment = getSegment(req);
const params = toSegmentParams(segment);
const search = paths(config!).api.catalog_system.pub.products.search;

const [product] = await fetchAPI<LegacyProduct[]>(
`${search.term(`${slug}/p`)}?${params}`,
{
withProxyCache: true,
headers: withSegmentCookie(segment),
},
);

return { product };
}