Skip to content

Commit 98cb623

Browse files
committed
feat: add BundledCampaign
1 parent d2ca5a9 commit 98cb623

File tree

2 files changed

+96
-2
lines changed

2 files changed

+96
-2
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { nostojs } from "@nosto/nosto-js"
2+
import { postJSON } from "@/utils/fetch"
3+
import { customElement } from "../decorators"
4+
import { NostoElement } from "../Element"
5+
import { addRequest } from "../Campaign/orchestrator"
6+
import { JSONResult } from "@nosto/nosto-js/client"
7+
8+
/**
9+
* BundledCampaign is a custom element that fetches Nosto placement results and renders the results
10+
* using a Shopify section template via the Bundled Section Rendering API.
11+
* Campaign result metadata is persisted in cart attributes
12+
*
13+
* @property {string} placement - The placement identifier for the campaign.
14+
* @property {string} section - The section to be used for Section Rendering API based rendering.
15+
*/
16+
@customElement("nosto-bundled-campaign")
17+
export class BundledCampaign extends NostoElement {
18+
/** @private */
19+
static attributes = {
20+
placement: String,
21+
handles: String,
22+
section: String
23+
}
24+
25+
placement!: string
26+
handles!: string
27+
section!: string
28+
29+
async connectedCallback() {
30+
this.toggleAttribute("loading", true)
31+
try {
32+
await this.#initializeMarkup()
33+
} finally {
34+
this.toggleAttribute("loading", false)
35+
}
36+
}
37+
38+
async #initializeMarkup() {
39+
const api = await new Promise(nostojs)
40+
const rec = (await addRequest({
41+
placement: this.placement,
42+
responseMode: "JSON_ORIGINAL" // TODO use a responseMode that returns only the needed data
43+
})) as JSONResult
44+
if (!rec) {
45+
return
46+
}
47+
const handles = rec.products.map(product => product.handle).join(":")
48+
if (this.handles !== handles) {
49+
const markup = await getSectionMarkup(this, handles, rec)
50+
this.innerHTML = markup
51+
}
52+
api.attributeProductClicksInCampaign(this, rec)
53+
}
54+
}
55+
56+
async function getSectionMarkup(element: BundledCampaign, handles: string, rec: JSONResult) {
57+
const target = new URL("/cart/update.js", window.location.href)
58+
const payload = {
59+
attributes: {
60+
[`nosto_${element.placement}_title`]: rec.title || "",
61+
[`nosto_${element.placement}_handles`]: handles
62+
},
63+
sections: element.section,
64+
}
65+
const sectionHtml = await postJSON(target.href, payload)
66+
const parser = new DOMParser()
67+
const doc = parser.parseFromString(sectionHtml, "text/html")
68+
return doc.querySelector(`nosto-bundled-campaign[placement="${element.placement}"]`)?.innerHTML || ""
69+
}
70+
71+
declare global {
72+
interface HTMLElementTagNameMap {
73+
"nosto-bundled-campaign": BundledCampaign
74+
}
75+
}

src/utils/fetch.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
/**
22
* Internal function to handle common fetch logic with error checking.
33
* @param url - The URL to fetch
4+
* @param options - Optional fetch options
45
* @returns Promise that resolves to the Response object
56
* @throws Error if the fetch request fails
67
*/
7-
async function fetchWithErrorHandling(url: string) {
8-
const response = await fetch(url)
8+
async function fetchWithErrorHandling(url: string, options?: RequestInit) {
9+
const response = await fetch(url, options)
910
if (!response.ok) {
1011
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`)
1112
}
@@ -33,3 +34,21 @@ export async function getJSON(url: string) {
3334
const response = await fetchWithErrorHandling(url)
3435
return response.json()
3536
}
37+
38+
/**
39+
* Posts a JSON payload to a URL and returns the response as a JSON object.
40+
* @param url - The URL to post to
41+
* @param data - The JSON payload to send
42+
* @returns Promise that resolves to the parsed JSON response
43+
* @throws Error if the fetch request fails or JSON parsing fails
44+
*/
45+
export async function postJSON(url: string, data: object) {
46+
const response = await fetchWithErrorHandling(url, {
47+
method: "POST",
48+
headers: {
49+
"Content-Type": "application/json"
50+
},
51+
body: JSON.stringify(data)
52+
})
53+
return response.json()
54+
}

0 commit comments

Comments
 (0)