@decocms/apps 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/.github/workflows/release.yml +34 -0
  2. package/.releaserc.json +25 -0
  3. package/commerce/components/Image.tsx +209 -0
  4. package/commerce/components/JsonLd.tsx +285 -0
  5. package/commerce/sdk/analytics.ts +24 -0
  6. package/commerce/sdk/formatPrice.ts +23 -0
  7. package/commerce/sdk/url.ts +9 -0
  8. package/commerce/sdk/useOffer.ts +75 -0
  9. package/commerce/sdk/useVariantPossibilities.ts +43 -0
  10. package/commerce/types/commerce.ts +1105 -0
  11. package/commerce/utils/canonical.ts +11 -0
  12. package/commerce/utils/constants.ts +9 -0
  13. package/commerce/utils/filters.ts +10 -0
  14. package/commerce/utils/productToAnalyticsItem.ts +67 -0
  15. package/commerce/utils/stateByZip.ts +50 -0
  16. package/knip.json +19 -0
  17. package/package.json +77 -0
  18. package/shopify/actions/cart/addItems.ts +37 -0
  19. package/shopify/actions/cart/updateCoupons.ts +32 -0
  20. package/shopify/actions/cart/updateItems.ts +32 -0
  21. package/shopify/actions/user/signIn.ts +45 -0
  22. package/shopify/actions/user/signUp.ts +36 -0
  23. package/shopify/client.ts +58 -0
  24. package/shopify/index.ts +32 -0
  25. package/shopify/init.ts +40 -0
  26. package/shopify/loaders/ProductDetailsPage.ts +35 -0
  27. package/shopify/loaders/ProductList.ts +101 -0
  28. package/shopify/loaders/ProductListingPage.ts +180 -0
  29. package/shopify/loaders/RelatedProducts.ts +45 -0
  30. package/shopify/loaders/cart.ts +73 -0
  31. package/shopify/loaders/shop.ts +40 -0
  32. package/shopify/loaders/user.ts +44 -0
  33. package/shopify/utils/admin/admin.ts +57 -0
  34. package/shopify/utils/admin/queries.ts +29 -0
  35. package/shopify/utils/cart.ts +28 -0
  36. package/shopify/utils/cookies.ts +85 -0
  37. package/shopify/utils/enums.ts +438 -0
  38. package/shopify/utils/graphql.ts +69 -0
  39. package/shopify/utils/storefront/queries.ts +530 -0
  40. package/shopify/utils/storefront/storefront.graphql.gen.ts +113 -0
  41. package/shopify/utils/transform.ts +436 -0
  42. package/shopify/utils/types.ts +191 -0
  43. package/shopify/utils/user.ts +23 -0
  44. package/shopify/utils/utils.ts +164 -0
  45. package/tsconfig.json +11 -0
  46. package/vtex/README.md +6 -0
  47. package/vtex/actions/address.ts +211 -0
  48. package/vtex/actions/auth.ts +337 -0
  49. package/vtex/actions/checkout.ts +497 -0
  50. package/vtex/actions/index.ts +11 -0
  51. package/vtex/actions/masterData.ts +170 -0
  52. package/vtex/actions/misc.ts +196 -0
  53. package/vtex/actions/newsletter.ts +108 -0
  54. package/vtex/actions/orders.ts +37 -0
  55. package/vtex/actions/profile.ts +119 -0
  56. package/vtex/actions/session.ts +87 -0
  57. package/vtex/actions/trigger.ts +43 -0
  58. package/vtex/actions/wishlist.ts +116 -0
  59. package/vtex/client.ts +423 -0
  60. package/vtex/hooks/index.ts +4 -0
  61. package/vtex/hooks/useAutocomplete.ts +89 -0
  62. package/vtex/hooks/useCart.ts +219 -0
  63. package/vtex/hooks/useUser.ts +78 -0
  64. package/vtex/hooks/useWishlist.ts +119 -0
  65. package/vtex/index.ts +14 -0
  66. package/vtex/inline-loaders/productDetailsPage.ts +75 -0
  67. package/vtex/inline-loaders/productList.ts +163 -0
  68. package/vtex/inline-loaders/productListingPage.ts +447 -0
  69. package/vtex/inline-loaders/relatedProducts.ts +83 -0
  70. package/vtex/inline-loaders/suggestions.ts +49 -0
  71. package/vtex/inline-loaders/workflowProducts.ts +68 -0
  72. package/vtex/invoke.ts +202 -0
  73. package/vtex/loaders/address.ts +120 -0
  74. package/vtex/loaders/brands.ts +51 -0
  75. package/vtex/loaders/cart.ts +49 -0
  76. package/vtex/loaders/catalog.ts +165 -0
  77. package/vtex/loaders/collections.ts +57 -0
  78. package/vtex/loaders/index.ts +19 -0
  79. package/vtex/loaders/legacy.ts +671 -0
  80. package/vtex/loaders/logistics.ts +115 -0
  81. package/vtex/loaders/navbar.ts +29 -0
  82. package/vtex/loaders/orders.ts +103 -0
  83. package/vtex/loaders/pageType.ts +62 -0
  84. package/vtex/loaders/payment.ts +107 -0
  85. package/vtex/loaders/profile.ts +138 -0
  86. package/vtex/loaders/promotion.ts +33 -0
  87. package/vtex/loaders/search.ts +127 -0
  88. package/vtex/loaders/session.ts +91 -0
  89. package/vtex/loaders/user.ts +89 -0
  90. package/vtex/loaders/wishlist.ts +89 -0
  91. package/vtex/loaders/wishlistProducts.ts +81 -0
  92. package/vtex/loaders/workflow.ts +323 -0
  93. package/vtex/logo.png +0 -0
  94. package/vtex/middleware.ts +229 -0
  95. package/vtex/types.ts +248 -0
  96. package/vtex/utils/batch.ts +21 -0
  97. package/vtex/utils/cookies.ts +76 -0
  98. package/vtex/utils/enrichment.ts +540 -0
  99. package/vtex/utils/fetchCache.ts +150 -0
  100. package/vtex/utils/index.ts +17 -0
  101. package/vtex/utils/intelligentSearch.ts +84 -0
  102. package/vtex/utils/legacy.ts +155 -0
  103. package/vtex/utils/pickAndOmit.ts +30 -0
  104. package/vtex/utils/proxy.ts +196 -0
  105. package/vtex/utils/resourceRange.ts +10 -0
  106. package/vtex/utils/segment.ts +163 -0
  107. package/vtex/utils/similars.ts +38 -0
  108. package/vtex/utils/sitemap.ts +133 -0
  109. package/vtex/utils/slugCache.ts +32 -0
  110. package/vtex/utils/slugify.ts +13 -0
  111. package/vtex/utils/transform.ts +1331 -0
  112. package/vtex/utils/types.ts +1884 -0
  113. package/vtex/utils/vtexId.ts +103 -0
@@ -0,0 +1,34 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: write
10
+ packages: write
11
+ issues: write
12
+ pull-requests: write
13
+
14
+ jobs:
15
+ release:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ with:
20
+ fetch-depth: 0
21
+
22
+ - uses: actions/setup-node@v4
23
+ with:
24
+ node-version: 22
25
+ registry-url: https://npm.pkg.github.com
26
+ scope: "@decocms"
27
+
28
+ - run: npm install
29
+
30
+ - name: Release
31
+ run: npx semantic-release
32
+ env:
33
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34
+ NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,25 @@
1
+ {
2
+ "branches": ["main"],
3
+ "plugins": [
4
+ ["@semantic-release/commit-analyzer", {
5
+ "preset": "angular",
6
+ "releaseRules": [
7
+ { "type": "feat", "release": "minor" },
8
+ { "type": "fix", "release": "patch" },
9
+ { "type": "perf", "release": "patch" },
10
+ { "type": "refactor", "release": "patch" },
11
+ { "type": "docs", "release": false },
12
+ { "type": "chore", "release": false },
13
+ { "type": "style", "release": false },
14
+ { "type": "test", "release": false }
15
+ ]
16
+ }],
17
+ "@semantic-release/release-notes-generator",
18
+ "@semantic-release/npm",
19
+ "@semantic-release/github",
20
+ ["@semantic-release/git", {
21
+ "assets": ["package.json"],
22
+ "message": "chore(release): ${nextRelease.version} [skip ci]"
23
+ }]
24
+ ]
25
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Optimized Image component with CDN-aware transforms.
3
+ *
4
+ * Generates responsive srcset, enforces width/height for CLS prevention,
5
+ * and builds optimized URLs for different image CDNs (VTEX, Shopify,
6
+ * Deco, Cloudflare).
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * <Image
11
+ * src="https://store.vteximg.com.br/products/123.jpg"
12
+ * width={400}
13
+ * height={400}
14
+ * alt="Product name"
15
+ * cdn="vtex"
16
+ * />
17
+ * ```
18
+ */
19
+
20
+ import type { ImgHTMLAttributes } from "react";
21
+
22
+ export type ImageCDN = "vtex" | "shopify" | "deco" | "cloudflare" | "none";
23
+
24
+ export interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "width" | "height"> {
25
+ src: string;
26
+ width: number;
27
+ height: number;
28
+ /** Image CDN to use for URL transforms. @default "none" */
29
+ cdn?: ImageCDN;
30
+ /**
31
+ * Responsive sizes descriptor.
32
+ * @default "(max-width: 768px) 100vw, 50vw"
33
+ */
34
+ sizes?: string;
35
+ /**
36
+ * Multipliers for srcset generation.
37
+ * @default [1, 2]
38
+ */
39
+ srcSetMultipliers?: number[];
40
+ /** Preload the image (adds fetchPriority="high"). */
41
+ preload?: boolean;
42
+ }
43
+
44
+ // -------------------------------------------------------------------------
45
+ // CDN URL builders
46
+ // -------------------------------------------------------------------------
47
+
48
+ function vtexImageUrl(src: string, width: number, height: number): string {
49
+ if (src.includes("vteximg.com.br") || src.includes("vtexassets.com")) {
50
+ return src.replace(
51
+ /(-\d+-\d+)(\.\w+)$/,
52
+ `-${width}-${height}$2`,
53
+ );
54
+ }
55
+
56
+ const url = new URL(src, "https://placeholder.com");
57
+ url.searchParams.set("width", String(width));
58
+ url.searchParams.set("height", String(height));
59
+ return url.toString();
60
+ }
61
+
62
+ function shopifyImageUrl(src: string, width: number): string {
63
+ if (src.includes("cdn.shopify.com")) {
64
+ return src.replace(/(\.\w+)(\?.*)?$/, `_${width}x$1$2`);
65
+ }
66
+ return src;
67
+ }
68
+
69
+ function decoImageUrl(src: string, width: number, height: number): string {
70
+ if (src.includes("decocache.com") || src.includes("ozksgdmyrqcxcwhnbepg")) {
71
+ const url = new URL(src);
72
+ url.searchParams.set("width", String(width));
73
+ url.searchParams.set("height", String(height));
74
+ url.searchParams.set("fit", "cover");
75
+ return url.toString();
76
+ }
77
+ return src;
78
+ }
79
+
80
+ function cloudflareImageUrl(src: string, width: number, height: number): string {
81
+ return `/cdn-cgi/image/width=${width},height=${height},fit=cover,format=auto,quality=80/${src}`;
82
+ }
83
+
84
+ function buildUrl(src: string, width: number, height: number, cdn: ImageCDN): string {
85
+ switch (cdn) {
86
+ case "vtex": return vtexImageUrl(src, width, height);
87
+ case "shopify": return shopifyImageUrl(src, width);
88
+ case "deco": return decoImageUrl(src, width, height);
89
+ case "cloudflare": return cloudflareImageUrl(src, width, height);
90
+ default: return src;
91
+ }
92
+ }
93
+
94
+ function buildSrcSet(
95
+ src: string,
96
+ width: number,
97
+ height: number,
98
+ cdn: ImageCDN,
99
+ multipliers: number[],
100
+ ): string {
101
+ return multipliers
102
+ .map((m) => {
103
+ const w = Math.round(width * m);
104
+ const h = Math.round(height * m);
105
+ return `${buildUrl(src, w, h, cdn)} ${w}w`;
106
+ })
107
+ .join(", ");
108
+ }
109
+
110
+ // -------------------------------------------------------------------------
111
+ // Component
112
+ // -------------------------------------------------------------------------
113
+
114
+ export function Image({
115
+ src,
116
+ width,
117
+ height,
118
+ cdn = "none",
119
+ sizes = "(max-width: 768px) 100vw, 50vw",
120
+ srcSetMultipliers = [1, 2],
121
+ preload,
122
+ loading,
123
+ decoding,
124
+ ...rest
125
+ }: ImageProps) {
126
+ const optimizedSrc = buildUrl(src, width, height, cdn);
127
+ const srcSet = cdn !== "none"
128
+ ? buildSrcSet(src, width, height, cdn, srcSetMultipliers)
129
+ : undefined;
130
+
131
+ return (
132
+ <img
133
+ src={optimizedSrc}
134
+ srcSet={srcSet}
135
+ sizes={srcSet ? sizes : undefined}
136
+ width={width}
137
+ height={height}
138
+ loading={loading ?? (preload ? "eager" : "lazy")}
139
+ decoding={decoding ?? "async"}
140
+ fetchPriority={preload ? "high" : undefined}
141
+ {...rest}
142
+ />
143
+ );
144
+ }
145
+
146
+ // -------------------------------------------------------------------------
147
+ // Picture (responsive art direction)
148
+ // -------------------------------------------------------------------------
149
+
150
+ export interface PictureSource {
151
+ src: string;
152
+ width: number;
153
+ height: number;
154
+ media: string;
155
+ cdn?: ImageCDN;
156
+ }
157
+
158
+ export interface PictureProps extends Omit<ImageProps, "sizes"> {
159
+ sources: PictureSource[];
160
+ }
161
+
162
+ /**
163
+ * Picture component for responsive art direction.
164
+ *
165
+ * @example
166
+ * ```tsx
167
+ * <Picture
168
+ * sources={[
169
+ * { src: mobileSrc, width: 375, height: 200, media: "(max-width: 767px)", cdn: "vtex" },
170
+ * { src: desktopSrc, width: 1200, height: 400, media: "(min-width: 768px)", cdn: "vtex" },
171
+ * ]}
172
+ * src={desktopSrc}
173
+ * width={1200}
174
+ * height={400}
175
+ * alt="Banner"
176
+ * />
177
+ * ```
178
+ */
179
+ export function Picture({
180
+ sources,
181
+ src,
182
+ width,
183
+ height,
184
+ cdn = "none",
185
+ preload,
186
+ ...rest
187
+ }: PictureProps) {
188
+ return (
189
+ <picture>
190
+ {sources.map((source, i) => (
191
+ <source
192
+ key={i}
193
+ srcSet={buildUrl(source.src, source.width, source.height, source.cdn ?? cdn)}
194
+ media={source.media}
195
+ width={source.width}
196
+ height={source.height}
197
+ />
198
+ ))}
199
+ <Image
200
+ src={src}
201
+ width={width}
202
+ height={height}
203
+ cdn={cdn}
204
+ preload={preload}
205
+ {...rest}
206
+ />
207
+ </picture>
208
+ );
209
+ }
@@ -0,0 +1,285 @@
1
+ /**
2
+ * SEO JSON-LD structured data components.
3
+ *
4
+ * Generates JSON-LD script tags for Product (PDP), ProductList (PLP),
5
+ * and BreadcrumbList schemas. Compatible with Google's Rich Results
6
+ * requirements.
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * import { ProductJsonLd, PLPJsonLd, BreadcrumbJsonLd } from "@decocms/apps/commerce/components/JsonLd";
11
+ *
12
+ * // In a PDP route
13
+ * <ProductJsonLd product={product} />
14
+ *
15
+ * // In a PLP route
16
+ * <PLPJsonLd page={productListingPage} />
17
+ *
18
+ * // Anywhere with breadcrumbs
19
+ * <BreadcrumbJsonLd breadcrumb={breadcrumbList} />
20
+ * ```
21
+ */
22
+
23
+ import type {
24
+ Product,
25
+ ProductListingPage,
26
+ BreadcrumbList,
27
+ ListItem,
28
+ Offer,
29
+ AggregateOffer,
30
+ UnitPriceSpecification,
31
+ AggregateRating,
32
+ } from "../types/commerce";
33
+
34
+ // -------------------------------------------------------------------------
35
+ // JSON-LD script renderer
36
+ // -------------------------------------------------------------------------
37
+
38
+ function JsonLdScript({ data }: { data: unknown }) {
39
+ return (
40
+ <script
41
+ type="application/ld+json"
42
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
43
+ />
44
+ );
45
+ }
46
+
47
+ // -------------------------------------------------------------------------
48
+ // Product (PDP)
49
+ // -------------------------------------------------------------------------
50
+
51
+ export interface ProductJsonLdProps {
52
+ product: Product;
53
+ /** Override the canonical URL. Defaults to product.url. */
54
+ url?: string;
55
+ }
56
+
57
+ function getBestOffer(offers: Offer[] | AggregateOffer | undefined): {
58
+ price?: number;
59
+ priceCurrency?: string;
60
+ availability?: string;
61
+ seller?: string;
62
+ priceValidUntil?: string;
63
+ } {
64
+ if (!offers) return {};
65
+
66
+ if ("@type" in offers && offers["@type"] === "AggregateOffer") {
67
+ const agg = offers as AggregateOffer;
68
+ return {
69
+ price: agg.lowPrice,
70
+ priceCurrency: agg.priceCurrency,
71
+ };
72
+ }
73
+
74
+ if (Array.isArray(offers) && offers.length > 0) {
75
+ const best = offers.reduce((a, b) => {
76
+ const ap = a.price ?? Infinity;
77
+ const bp = b.price ?? Infinity;
78
+ return ap <= bp ? a : b;
79
+ });
80
+ return {
81
+ price: best.price,
82
+ priceCurrency: best.priceCurrency,
83
+ availability: best.availability,
84
+ seller: best.seller,
85
+ priceValidUntil: best.priceValidUntil,
86
+ };
87
+ }
88
+
89
+ return {};
90
+ }
91
+
92
+ function getListPrice(
93
+ priceSpec: UnitPriceSpecification[] | undefined,
94
+ ): number | undefined {
95
+ if (!priceSpec) return undefined;
96
+ const list = priceSpec.find(
97
+ (p) =>
98
+ p.priceType === "https://schema.org/ListPrice" ||
99
+ p.priceType === "https://schema.org/SRP",
100
+ );
101
+ return list?.price;
102
+ }
103
+
104
+ export function ProductJsonLd({ product, url }: ProductJsonLdProps) {
105
+ const offer = getBestOffer(product.offers as Offer[] | AggregateOffer | undefined);
106
+ const images = product.image?.map((img) => img.url).filter(Boolean) ?? [];
107
+ const rating = product.aggregateRating as AggregateRating | undefined;
108
+
109
+ const data: Record<string, unknown> = {
110
+ "@context": "https://schema.org",
111
+ "@type": "Product",
112
+ name: product.name,
113
+ description: product.description,
114
+ image: images.length === 1 ? images[0] : images,
115
+ url: url ?? product.url,
116
+ sku: product.sku,
117
+ productID: product.productID,
118
+ brand: product.brand
119
+ ? { "@type": "Brand", name: product.brand.name }
120
+ : undefined,
121
+ gtin: product.gtin,
122
+ };
123
+
124
+ if (offer.price != null) {
125
+ data.offers = {
126
+ "@type": "Offer",
127
+ price: offer.price,
128
+ priceCurrency: offer.priceCurrency ?? "BRL",
129
+ availability: offer.availability ?? "https://schema.org/InStock",
130
+ seller: offer.seller
131
+ ? { "@type": "Organization", name: offer.seller }
132
+ : undefined,
133
+ priceValidUntil: offer.priceValidUntil,
134
+ url: url ?? product.url,
135
+ };
136
+ }
137
+
138
+ if (rating && rating.ratingValue) {
139
+ data.aggregateRating = {
140
+ "@type": "AggregateRating",
141
+ ratingValue: rating.ratingValue,
142
+ reviewCount: rating.reviewCount ?? rating.ratingCount ?? 0,
143
+ bestRating: rating.bestRating ?? 5,
144
+ worstRating: rating.worstRating ?? 1,
145
+ };
146
+ }
147
+
148
+ return <JsonLdScript data={data} />;
149
+ }
150
+
151
+ // -------------------------------------------------------------------------
152
+ // Product Listing Page (PLP)
153
+ // -------------------------------------------------------------------------
154
+
155
+ export interface PLPJsonLdProps {
156
+ page: ProductListingPage;
157
+ /** Override the canonical URL. */
158
+ url?: string;
159
+ }
160
+
161
+ export function PLPJsonLd({ page, url }: PLPJsonLdProps) {
162
+ const items = (page.products ?? []).map((product, index) => {
163
+ const offer = getBestOffer(product.offers as Offer[] | AggregateOffer | undefined);
164
+ return {
165
+ "@type": "ListItem" as const,
166
+ position: index + 1,
167
+ item: {
168
+ "@type": "Product" as const,
169
+ name: product.name,
170
+ url: product.url,
171
+ image: product.image?.[0]?.url,
172
+ offers: offer.price != null
173
+ ? {
174
+ "@type": "Offer" as const,
175
+ price: offer.price,
176
+ priceCurrency: offer.priceCurrency ?? "BRL",
177
+ availability: offer.availability ?? "https://schema.org/InStock",
178
+ }
179
+ : undefined,
180
+ },
181
+ };
182
+ });
183
+
184
+ const data = {
185
+ "@context": "https://schema.org",
186
+ "@type": "ItemList",
187
+ url: url ?? page.seo?.canonical,
188
+ name: page.seo?.title,
189
+ description: page.seo?.description,
190
+ numberOfItems: page.products?.length ?? 0,
191
+ itemListElement: items,
192
+ };
193
+
194
+ return <JsonLdScript data={data} />;
195
+ }
196
+
197
+ // -------------------------------------------------------------------------
198
+ // Breadcrumb
199
+ // -------------------------------------------------------------------------
200
+
201
+ export interface BreadcrumbJsonLdProps {
202
+ breadcrumb: BreadcrumbList;
203
+ }
204
+
205
+ export function BreadcrumbJsonLd({ breadcrumb }: BreadcrumbJsonLdProps) {
206
+ const items = (breadcrumb.itemListElement ?? []).map((item, index) => {
207
+ const listItem = item as ListItem;
208
+ return {
209
+ "@type": "ListItem" as const,
210
+ position: listItem.position ?? index + 1,
211
+ name: listItem.name,
212
+ item: listItem.item ?? listItem.url,
213
+ };
214
+ });
215
+
216
+ const data = {
217
+ "@context": "https://schema.org",
218
+ "@type": "BreadcrumbList",
219
+ itemListElement: items,
220
+ numberOfItems: items.length,
221
+ };
222
+
223
+ return <JsonLdScript data={data} />;
224
+ }
225
+
226
+ // -------------------------------------------------------------------------
227
+ // Generic SEO Meta
228
+ // -------------------------------------------------------------------------
229
+
230
+ export interface SeoMetaProps {
231
+ title?: string;
232
+ description?: string;
233
+ canonical?: string;
234
+ image?: string;
235
+ noIndex?: boolean;
236
+ type?: "website" | "article" | "product";
237
+ siteName?: string;
238
+ }
239
+
240
+ /**
241
+ * Generates Open Graph and Twitter Card meta tags.
242
+ *
243
+ * Use this in combination with TanStack Router's `meta()` route option,
244
+ * or render directly in the component tree (tags will be hoisted to <head>
245
+ * by React's built-in behavior with TanStack Start).
246
+ */
247
+ export function seoMetaTags(props: SeoMetaProps): Array<Record<string, string>> {
248
+ const tags: Array<Record<string, string>> = [];
249
+
250
+ if (props.title) {
251
+ tags.push({ title: props.title });
252
+ tags.push({ property: "og:title", content: props.title });
253
+ tags.push({ name: "twitter:title", content: props.title });
254
+ }
255
+
256
+ if (props.description) {
257
+ tags.push({ name: "description", content: props.description });
258
+ tags.push({ property: "og:description", content: props.description });
259
+ tags.push({ name: "twitter:description", content: props.description });
260
+ }
261
+
262
+ if (props.canonical) {
263
+ tags.push({ property: "og:url", content: props.canonical });
264
+ }
265
+
266
+ if (props.image) {
267
+ tags.push({ property: "og:image", content: props.image });
268
+ tags.push({ name: "twitter:image", content: props.image });
269
+ tags.push({ name: "twitter:card", content: "summary_large_image" });
270
+ }
271
+
272
+ if (props.type) {
273
+ tags.push({ property: "og:type", content: props.type });
274
+ }
275
+
276
+ if (props.siteName) {
277
+ tags.push({ property: "og:site_name", content: props.siteName });
278
+ }
279
+
280
+ if (props.noIndex) {
281
+ tags.push({ name: "robots", content: "noindex, nofollow" });
282
+ }
283
+
284
+ return tags;
285
+ }
@@ -0,0 +1,24 @@
1
+ import type { AnalyticsEvent } from "../types/commerce";
2
+
3
+ declare global {
4
+ interface Window {
5
+ DECO: { events: { dispatch: (event: any) => void } };
6
+ DECO_SITES_STD: {
7
+ sendAnalyticsEvent: (args: AnalyticsEvent) => void;
8
+ };
9
+ }
10
+ }
11
+
12
+ export const sendEvent = <E extends AnalyticsEvent>(event: E) => {
13
+ if (typeof globalThis.window?.DECO?.events?.dispatch === "function") {
14
+ globalThis.window.DECO.events.dispatch(event);
15
+ return;
16
+ }
17
+
18
+ if (typeof globalThis.window?.DECO_SITES_STD?.sendAnalyticsEvent === "function") {
19
+ globalThis.window.DECO_SITES_STD.sendAnalyticsEvent(event);
20
+ return;
21
+ }
22
+
23
+ console.info("[analytics] No event dispatcher found. Event not sent:", event.name);
24
+ };
@@ -0,0 +1,23 @@
1
+ const formatters = new Map<string, Intl.NumberFormat>();
2
+
3
+ const formatter = (currency: string, locale: string) => {
4
+ const key = `${currency}::${locale}`;
5
+
6
+ if (!formatters.has(key)) {
7
+ formatters.set(
8
+ key,
9
+ new Intl.NumberFormat(locale, {
10
+ style: "currency",
11
+ currency,
12
+ }),
13
+ );
14
+ }
15
+
16
+ return formatters.get(key)!;
17
+ };
18
+
19
+ export const formatPrice = (
20
+ price: number | undefined,
21
+ currency = "BRL",
22
+ locale = "pt-BR",
23
+ ) => price ? formatter(currency, locale).format(price) : null;
@@ -0,0 +1,9 @@
1
+ export const relative = (link?: string | undefined) => {
2
+ if (!link) return undefined;
3
+ try {
4
+ const linkUrl = new URL(link, "https://localhost");
5
+ return `${linkUrl.pathname}${linkUrl.search}`;
6
+ } catch {
7
+ return link;
8
+ }
9
+ };
@@ -0,0 +1,75 @@
1
+ import type {
2
+ AggregateOffer,
3
+ UnitPriceSpecification,
4
+ } from "../types/commerce";
5
+
6
+ const bestInstallment = (
7
+ acc: UnitPriceSpecification | null,
8
+ curr: UnitPriceSpecification,
9
+ ) => {
10
+ if (curr.priceComponentType !== "https://schema.org/Installment") {
11
+ return acc;
12
+ }
13
+
14
+ if (!acc) {
15
+ return curr;
16
+ }
17
+
18
+ if (acc.price > curr.price) {
19
+ return curr;
20
+ }
21
+
22
+ if (acc.price < curr.price) {
23
+ return acc;
24
+ }
25
+
26
+ if (
27
+ acc.billingDuration && curr.billingDuration &&
28
+ acc.billingDuration < curr.billingDuration
29
+ ) {
30
+ return curr;
31
+ }
32
+
33
+ return acc;
34
+ };
35
+
36
+ const installmentToString = (
37
+ installment: UnitPriceSpecification,
38
+ locale = "pt-BR",
39
+ currency = "BRL",
40
+ ) => {
41
+ const { billingDuration, billingIncrement } = installment;
42
+
43
+ if (!billingDuration || !billingIncrement) {
44
+ return "";
45
+ }
46
+
47
+ const formatted = new Intl.NumberFormat(locale, {
48
+ style: "currency",
49
+ currency,
50
+ }).format(billingIncrement);
51
+
52
+ return `${billingDuration}x ${formatted}`;
53
+ };
54
+
55
+ export const useOffer = (aggregateOffer?: AggregateOffer) => {
56
+ const offer = aggregateOffer?.offers?.[0];
57
+ const listPrice = offer?.priceSpecification?.find((spec) =>
58
+ spec.priceType === "https://schema.org/ListPrice"
59
+ );
60
+ const installment = offer?.priceSpecification?.reduce(bestInstallment, null);
61
+ const seller = offer?.seller;
62
+ const price = offer?.price;
63
+ const availability = offer?.availability;
64
+
65
+ return {
66
+ price,
67
+ listPrice: listPrice?.price,
68
+ availability,
69
+ seller,
70
+ installments: installment && price
71
+ ? installmentToString(installment)
72
+ : null,
73
+ installment,
74
+ };
75
+ };