@nosto/nosto-react 1.0.0 → 2.0.0

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.
@@ -0,0 +1,21 @@
1
+ import { useEffect, useRef, useMemo } from "react"
2
+ import { deepCompare } from "../utils/compare"
3
+
4
+ export function useDeepCompareEffect(
5
+ callback: Parameters<typeof useEffect>[0],
6
+ dependencies: Parameters<typeof useEffect>[1]
7
+ ): ReturnType<typeof useEffect> {
8
+ return useEffect(callback, useDeepCompareMemoize(dependencies))
9
+ }
10
+
11
+ function useDeepCompareMemoize<T>(value: T) {
12
+ const ref = useRef<T>(value);
13
+ const signalRef = useRef<number>(0)
14
+
15
+ if (!deepCompare(value, ref.current)) {
16
+ ref.current = value
17
+ signalRef.current += 1
18
+ }
19
+
20
+ return useMemo(() => ref.current, [signalRef.current])
21
+ }
@@ -0,0 +1,22 @@
1
+ import { useEffect } from "react"
2
+ import { useNostoContext } from "./useNostoContext"
3
+ import { NostoClient } from "../types"
4
+ import { useDeepCompareEffect } from "./useDeepCompareEffect"
5
+
6
+ export function useNostoApi(
7
+ cb: (api: NostoClient) => void,
8
+ deps?: React.DependencyList,
9
+ flags?: { deep?: boolean }
10
+ ): void {
11
+ const { clientScriptLoaded, currentVariation, responseMode } = useNostoContext()
12
+ const useEffectFn = flags?.deep ? useDeepCompareEffect : useEffect
13
+
14
+ useEffectFn(() => {
15
+ if (clientScriptLoaded) {
16
+ window.nostojs(api => {
17
+ api.defaultSession().setVariation(currentVariation!).setResponseMode(responseMode)
18
+ cb(api)
19
+ })
20
+ }
21
+ }, [clientScriptLoaded, currentVariation, responseMode, ...(deps ?? [])])
22
+ }
@@ -0,0 +1,18 @@
1
+ import { useContext } from "react"
2
+ import { NostoContext, NostoContextType } from "../context"
3
+
4
+ /**
5
+ * A hook that allows you to access the NostoContext and retrieve Nosto-related data from it in React components.
6
+ *
7
+ * @group Essential Functions
8
+ */
9
+ export function useNostoContext(): NostoContextType {
10
+ const context = useContext(NostoContext)
11
+
12
+ if (!context) {
13
+ throw new Error("No nosto context found")
14
+ }
15
+
16
+ return context
17
+ }
18
+
@@ -0,0 +1,60 @@
1
+ import { useRef } from "react"
2
+ import { createRoot, Root } from "react-dom/client"
3
+ import { ActionResponse, Recommendation } from "../types"
4
+ import { useNostoContext } from "./useNostoContext"
5
+ import React from "react"
6
+ import { RecommendationComponent } from "../context"
7
+
8
+ // RecommendationComponent for client-side rendering:
9
+ function RecommendationComponentWrapper(props: {
10
+ recommendationComponent: RecommendationComponent,
11
+ nostoRecommendation: Recommendation }) {
12
+
13
+ return React.cloneElement(props.recommendationComponent, {
14
+ // eslint-disable-next-line react/prop-types
15
+ nostoRecommendation: props.nostoRecommendation,
16
+ })
17
+ }
18
+
19
+ function injectCampaigns(data: ActionResponse) {
20
+ if (!window.nostojs) {
21
+ throw new Error("Nosto has not yet been initialized")
22
+ }
23
+ window.nostojs(api => {
24
+ api.placements.injectCampaigns(data.recommendations)
25
+ })
26
+ }
27
+
28
+ export function useRenderCampaigns() {
29
+ const { responseMode, recommendationComponent } = useNostoContext()
30
+ const placementRefs = useRef<Record<string, Root>>({})
31
+
32
+ if (responseMode == "HTML") {
33
+ return { renderCampaigns: injectCampaigns }
34
+ }
35
+
36
+ function renderCampaigns(data: ActionResponse) {
37
+ // render recommendation component into placements:
38
+ const recommendations = data.campaigns?.recommendations ?? {}
39
+ for (const key in recommendations) {
40
+ const recommendation = recommendations[key] as Recommendation
41
+ const placementSelector = "#" + key
42
+ const placementElement = document.querySelector(placementSelector)
43
+
44
+ if (placementElement) {
45
+ if (!placementRefs.current[key]) {
46
+ placementRefs.current[key] = createRoot(placementElement)
47
+ }
48
+ const root = placementRefs.current[key]!
49
+ root.render(
50
+ <RecommendationComponentWrapper
51
+ recommendationComponent={recommendationComponent!}
52
+ nostoRecommendation={recommendation}
53
+ ></RecommendationComponentWrapper>
54
+ )
55
+ }
56
+ }
57
+ }
58
+
59
+ return { renderCampaigns }
60
+ }
package/src/index.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export type { Cart, Customer, Product, Order, Recommendation } from "./types"
2
2
  export * from "./components"
3
+ export { NostoContext, type NostoContextType } from "./context"
4
+ export { useNostoContext } from "./hooks/useNostoContext"
package/src/types.ts CHANGED
@@ -17,6 +17,7 @@ declare global {
17
17
  export interface NostoClient {
18
18
  setAutoLoad(autoload: boolean): void
19
19
  defaultSession(): Session
20
+ listen(event: string, callback: (data: unknown) => void): void
20
21
  placements: {
21
22
  getPlacements(): string[]
22
23
  injectCampaigns(recommendations: Record<string, unknown>): void
@@ -28,7 +29,7 @@ export interface NostoClient {
28
29
  */
29
30
  export interface Recommendation {
30
31
  result_id: string
31
- products: Product[]
32
+ products: PushedProduct[]
32
33
  result_type: string
33
34
  title: string
34
35
  div_id: string
@@ -36,6 +37,77 @@ export interface Recommendation {
36
37
  params: unknown
37
38
  }
38
39
 
40
+ export interface PushedProduct {
41
+ age_group?: string
42
+ alternate_image_urls: string[]
43
+ availability: string
44
+ brand?: string
45
+ category: string[]
46
+ category_id: string[]
47
+ condition?: string
48
+ custom_fields: { [index: string]: string }
49
+ date_published?: Date
50
+ description?: string
51
+ gender?: string
52
+ google_category?: string
53
+ gtin?: string
54
+ image_url?: string
55
+ inventory_level?: number
56
+ list_price?: number
57
+ name: string
58
+ parent_category_id: string[]
59
+ price: number
60
+ price_currency_code: string
61
+ product_id: string
62
+ rating_value?: number
63
+ review_count?: number
64
+ skus: PushedProductSKU[]
65
+ source_updated?: Date
66
+ supplier_cost?: number
67
+ tags1: string[]
68
+ tags2: string[]
69
+ tags3: string[]
70
+ thumb_url?: string
71
+ unit_pricing_base_measure?: number
72
+ unit_pricing_measure?: number
73
+ unit_pricing_unit?: string
74
+ update_received?: Date
75
+ url: string
76
+ variation_id?: string
77
+ variations: { [index: string]: PushedVariation }
78
+ }
79
+
80
+ export interface PushedProductSKU extends NostoSku { }
81
+
82
+ export interface PushedVariation extends NostoVariant { }
83
+
84
+
85
+ export interface NostoSku extends Sku {
86
+ inventory_level?: number
87
+ }
88
+
89
+ export interface NostoVariant {
90
+ availability: string
91
+ available: boolean
92
+ discounted: boolean
93
+ list_price?: number
94
+ price: number
95
+ price_currency_code: string
96
+ price_text?: string
97
+ }
98
+
99
+ export interface Sku {
100
+ availability: string
101
+ custom_fields: { [index: string]: string }
102
+ gtin?: string
103
+ id: string
104
+ image_url?: string
105
+ list_price?: number
106
+ name: string
107
+ price: number
108
+ url?: string
109
+ }
110
+
39
111
  // copied from client script d.ts export
40
112
  declare const eventTypes: readonly ["vp", "lp", "dp", "rp", "bp", "vc", "or", "is", "cp", "ec", "es", "gc", "src", "cpr", "pl", "cc", "con"]
41
113
  declare type EventType = typeof eventTypes[number]
@@ -1,55 +0,0 @@
1
- import { createContext, useContext } from "react"
2
- import { NostoClient, Recommendation, RenderMode } from "../types"
3
-
4
- type AnyFunction = (...args: unknown[]) => unknown
5
-
6
- /**
7
- * @group Types
8
- */
9
- export interface NostoContextType {
10
- account: string
11
- clientScriptLoaded: boolean
12
- currentVariation?: string
13
- renderFunction?: AnyFunction
14
- responseMode: RenderMode
15
- recommendationComponent?: React.ReactElement<{
16
- nostoRecommendation: Recommendation
17
- }>
18
- useRenderCampaigns(type: string): {
19
- renderCampaigns(data: unknown, api: NostoClient): void
20
- pageTypeUpdated: boolean
21
- }
22
- pageType: string
23
- }
24
-
25
- /**
26
- * @group Essential Functions
27
- */
28
- export const NostoContext = createContext<NostoContextType>({
29
- account: "",
30
- currentVariation: "",
31
- pageType: "",
32
- responseMode: "HTML",
33
- clientScriptLoaded: false,
34
- useRenderCampaigns: () => {
35
- return {
36
- renderCampaigns: () => {},
37
- pageTypeUpdated: false,
38
- }
39
- },
40
- })
41
-
42
- /**
43
- * A hook that allows you to access the NostoContext and retrieve Nosto-related data from it in React components.
44
- *
45
- * @group Essential Functions
46
- */
47
- export function useNostoContext(): NostoContextType {
48
- const context = useContext(NostoContext)
49
-
50
- if (!context) {
51
- throw new Error("No nosto context found")
52
- }
53
-
54
- return context
55
- }
@@ -1,41 +0,0 @@
1
- import { useEffect, useRef, useMemo } from "react"
2
- import { useNostoContext } from "../components/context"
3
- import { deepCompare } from "./compare"
4
- import { NostoClient } from "../types"
5
-
6
- export function useDeepCompareEffect(
7
- callback: Parameters<typeof useEffect>[0],
8
- dependencies: Parameters<typeof useEffect>[1]
9
- ): ReturnType<typeof useEffect> {
10
- return useEffect(callback, useDeepCompareMemoize(dependencies))
11
- }
12
-
13
- function useDeepCompareMemoize<T>(value: T) {
14
- const ref = useRef<T>(value)
15
- const signalRef = useRef<number>(0)
16
-
17
- if (!deepCompare(value, ref.current)) {
18
- ref.current = value
19
- signalRef.current += 1
20
- }
21
-
22
- return useMemo(() => ref.current, [signalRef.current])
23
- }
24
-
25
- export function useNostoApi(
26
- cb: (api: NostoClient) => void,
27
- deps?: React.DependencyList,
28
- flags?: { deep?: boolean }
29
- ): void {
30
- const { clientScriptLoaded, currentVariation, responseMode } = useNostoContext()
31
- const useEffectFn = flags?.deep ? useDeepCompareEffect : useEffect
32
-
33
- useEffectFn(() => {
34
- if (clientScriptLoaded) {
35
- window.nostojs(api => {
36
- api.defaultSession().setVariation(currentVariation!).setResponseMode(responseMode)
37
- cb(api)
38
- })
39
- }
40
- }, [clientScriptLoaded, currentVariation, responseMode, ...(deps ?? [])])
41
- }