diff --git a/src/index.ts b/src/index.ts index d13b040..64c22c4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,8 @@ +import { Task } from "@lit/task"; import { LitElement, type TemplateResult, css, html } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; +import { customElement, property } from "lit/decorators.js"; import { TopsortConfigurationError, TopsortRequestError } from "./errors"; +import type { Auction, Banner, BannerState } from "./types"; /* Set up global environment for TS_BANNERS */ @@ -9,7 +11,7 @@ declare global { TS_BANNERS: { getLink(banner: Banner): string; getLoadingElement(): HTMLElement; - getErrorElement(error: Error): HTMLElement; + getErrorElement(error: unknown): HTMLElement; getNoWinnersElement(): HTMLElement; getBannerElement(banner: Banner): HTMLElement; }; @@ -44,50 +46,6 @@ const getDeviceType = (): "mobile" | "desktop" => { return "desktop"; }; -interface Loading { - status: "loading"; -} - -interface Errored { - status: "errored"; - error: Error; -} - -interface NoWinners { - status: "nowinners"; -} - -interface Ready { - status: "ready"; - banner: Banner; -} - -interface Auction { - type: "banners"; - slots: 1; - device: "mobile" | "desktop"; - slotId: string; - category?: { - id?: string; - ids?: string[]; - disjunctions?: string[][]; - }; - geoTargeting?: { - location: string; - }; - searchQuery?: string; -} - -/** The banner object returned from the auction request */ -export interface Banner { - type: "product" | "vendor" | "brand" | "url"; - id: string; - resolvedBidId: string; - asset: [{ url: string }]; -} - -type BannerState = Loading | Errored | NoWinners | Ready; - /** * A banner web component that runs an auction and renders the winning banner. */ @@ -120,10 +78,10 @@ export class TopsortBanner extends LitElement { @property({ attribute: "target", type: String }) readonly target?: string; - @state() - private state: BannerState = { - status: "loading", - }; + private task = new Task(this, { + task: this.runAuction, + args: () => [this.buildAuction()], + }); private getLink(banner: Banner): string { if (window.TS_BANNERS.getLink) { @@ -140,17 +98,17 @@ export class TopsortBanner extends LitElement { const element = window.TS_BANNERS.getLoadingElement(); return html`${element}`; } - return html`
`; + // By default, hide the component while loading + return html``; } - private getErrorElement(error: Error): TemplateResult { + private getErrorElement(error: unknown): TemplateResult { if (window.TS_BANNERS.getErrorElement) { const element = window.TS_BANNERS.getErrorElement(error); return html`${element}`; } - return html``; + // By default, hide the component if there are no winners + return html``; } private getBannerElement(banner: Banner): TemplateResult { @@ -182,131 +141,110 @@ export class TopsortBanner extends LitElement { `; } - private setState(state: BannerState) { - this.state = state; + private emitEvent(status: string) { const event = new CustomEvent("statechange", { - detail: { state, slotId: this.slotId }, + detail: { slotId: this.slotId, status }, bubbles: true, composed: true, }); this.dispatchEvent(event); } - private async runAuction() { + private buildAuction(): Auction { const device = getDeviceType(); - try { - const auction: Auction = { - type: "banners", - slots: 1, - device, - slotId: this.slotId, + const auction: Auction = { + type: "banners", + slots: 1, + device, + slotId: this.slotId, + }; + if (this.categoryId) { + auction.category = { + id: this.categoryId, }; - if (this.categoryId) { - auction.category = { - id: this.categoryId, - }; - } else if (this.categoryIds) { - auction.category = { - ids: this.categoryIds.split(",").map((item) => item.trim()), - }; - } else if (this.categoryDisjunctions) { - auction.category = { - disjunctions: [this.categoryDisjunctions.split(",").map((item) => item.trim())], - }; - } else if (this.searchQuery) { - auction.searchQuery = this.searchQuery; - } - if (this.location) { - auction.geoTargeting = { - location: this.location, - }; - } - const token = window.TS.token; - const url = window.TS.url || "https://api.topsort.com"; - const res = await fetch(new URL(`${url}/v2/auctions`), { - method: "POST", - mode: "cors", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - "X-UA": `topsort/banners-${import.meta.env.PACKAGE_VERSION} (${device}})`, - }, - body: JSON.stringify({ - auctions: [auction], - }), - }); - if (res.ok) { - const data = await res.json(); - const result = data.results[0]; - if (result) { - if (result.error) { - logError(result.error); - this.setState({ - status: "errored", - error: Error("Unknown Error"), - }); - } else if (result.winners[0]) { - const winner = result.winners[0]; - this.setState({ - status: "ready", - banner: winner, - }); - } else { - this.setState({ - status: "nowinners", - }); - } - } - } else { - const error = await res.json(); - logError(error); - this.setState({ - status: "errored", - error: new TopsortRequestError(error.message, res.status), - }); - } - } catch (err) { - logError(err); - if (err instanceof Error) { - this.setState({ - status: "errored", - error: err, - }); - } else { - this.setState({ - status: "errored", - error: Error("Unknown Error"), - }); - } + } else if (this.categoryIds) { + auction.category = { + ids: this.categoryIds.split(",").map((item) => item.trim()), + }; + } else if (this.categoryDisjunctions) { + auction.category = { + disjunctions: [this.categoryDisjunctions.split(",").map((item) => item.trim())], + }; + } else if (this.searchQuery) { + auction.searchQuery = this.searchQuery; } + if (this.location) { + auction.geoTargeting = { + location: this.location, + }; + } + return auction; } - // Runs when DOM is loaded. Much like React's `getInitialProps` - connectedCallback() { - super.connectedCallback(); - this.runAuction(); + private async runAuction( + [auction]: Auction[], + { signal }: { signal: AbortSignal }, + ): Promise