@nosto/nosto-react 0.5.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.
- package/README.md +1 -6
- package/dist/index.d.ts +145 -137
- package/dist/index.es.js +540 -761
- package/dist/index.umd.js +9 -9
- package/package.json +3 -1
- package/src/components/Nosto404.tsx +5 -17
- package/src/components/NostoCategory.tsx +5 -18
- package/src/components/NostoCheckout.tsx +5 -18
- package/src/components/NostoHome.tsx +5 -16
- package/src/components/NostoOrder.tsx +8 -20
- package/src/components/NostoOther.tsx +5 -17
- package/src/components/NostoProduct.tsx +6 -102
- package/src/components/NostoProvider.tsx +12 -68
- package/src/components/NostoSearch.tsx +5 -18
- package/src/components/NostoSession.tsx +3 -4
- package/src/components/helpers.ts +3 -0
- package/src/components/index.ts +0 -3
- package/src/context.ts +31 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useDeepCompareEffect.ts +21 -0
- package/src/hooks/useNostoApi.ts +22 -0
- package/src/hooks/useNostoContext.ts +18 -0
- package/src/hooks/useRenderCampaigns.tsx +60 -0
- package/src/index.ts +3 -2
- package/src/types.ts +863 -100
- package/src/components/context.ts +0 -55
- package/src/utils/hooks.ts +0 -41
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import React, { useEffect, isValidElement
|
|
2
|
-
import { NostoContext } from "
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import React, { useEffect, isValidElement } from "react"
|
|
2
|
+
import { NostoContext, RecommendationComponent } from "../context"
|
|
3
|
+
import { NostoClient } from "../types"
|
|
4
|
+
import { isNostoLoaded } from "./helpers"
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @group Components
|
|
@@ -19,7 +19,10 @@ export interface NostoProviderProps {
|
|
|
19
19
|
* Indicates an url of a server
|
|
20
20
|
*/
|
|
21
21
|
host?: string
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* children
|
|
24
|
+
*/
|
|
25
|
+
children: React.ReactElement | React.ReactElement[]
|
|
23
26
|
/**
|
|
24
27
|
* Indicates if merchant uses multiple currencies
|
|
25
28
|
*/
|
|
@@ -27,9 +30,7 @@ export interface NostoProviderProps {
|
|
|
27
30
|
/**
|
|
28
31
|
* Recommendation component which holds nostoRecommendation object
|
|
29
32
|
*/
|
|
30
|
-
recommendationComponent?:
|
|
31
|
-
nostoRecommendation: Recommendation
|
|
32
|
-
}>
|
|
33
|
+
recommendationComponent?: RecommendationComponent
|
|
33
34
|
/**
|
|
34
35
|
* Enables Shopify markets with language and market id
|
|
35
36
|
*/
|
|
@@ -67,7 +68,7 @@ export default function NostoProvider(props: NostoProviderProps) {
|
|
|
67
68
|
host,
|
|
68
69
|
children,
|
|
69
70
|
recommendationComponent,
|
|
70
|
-
shopifyMarkets
|
|
71
|
+
shopifyMarkets
|
|
71
72
|
} = props
|
|
72
73
|
const [clientScriptLoadedState, setClientScriptLoadedState] = React.useState(false)
|
|
73
74
|
const clientScriptLoaded = React.useMemo(() => clientScriptLoadedState, [clientScriptLoadedState])
|
|
@@ -78,61 +79,6 @@ export default function NostoProvider(props: NostoProviderProps) {
|
|
|
78
79
|
// Set responseMode for loading campaigns:
|
|
79
80
|
const responseMode = isValidElement(recommendationComponent) ? "JSON_ORIGINAL" : "HTML"
|
|
80
81
|
|
|
81
|
-
// RecommendationComponent for client-side rendering:
|
|
82
|
-
function RecommendationComponentWrapper(props: { nostoRecommendation: Recommendation }) {
|
|
83
|
-
return React.cloneElement(recommendationComponent!, {
|
|
84
|
-
// eslint-disable-next-line react/prop-types
|
|
85
|
-
nostoRecommendation: props.nostoRecommendation,
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// custom hook for rendering campaigns (CSR/SSR):
|
|
90
|
-
const [pageType, setPageType] = useState("")
|
|
91
|
-
function useRenderCampaigns(type: string = "") {
|
|
92
|
-
const placementRefs = useRef<Record<string, Root>>({})
|
|
93
|
-
useEffect(() => {
|
|
94
|
-
if (pageType !== type) {
|
|
95
|
-
setPageType(type)
|
|
96
|
-
}
|
|
97
|
-
}, [])
|
|
98
|
-
|
|
99
|
-
const pageTypeUpdated = type === pageType
|
|
100
|
-
|
|
101
|
-
function renderCampaigns(
|
|
102
|
-
data: {
|
|
103
|
-
recommendations: Record<string, Recommendation>
|
|
104
|
-
campaigns: {
|
|
105
|
-
recommendations: Record<string, Recommendation>
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
api: NostoClient
|
|
109
|
-
) {
|
|
110
|
-
if (responseMode == "HTML") {
|
|
111
|
-
// inject content campaigns as usual:
|
|
112
|
-
api.placements.injectCampaigns(data.recommendations)
|
|
113
|
-
} else {
|
|
114
|
-
// render recommendation component into placements:
|
|
115
|
-
const recommendations = data.campaigns.recommendations
|
|
116
|
-
for (const key in recommendations) {
|
|
117
|
-
const recommendation = recommendations[key]
|
|
118
|
-
const placementSelector = "#" + key
|
|
119
|
-
const placement = () => document.querySelector(placementSelector)
|
|
120
|
-
|
|
121
|
-
if (placement()) {
|
|
122
|
-
if (!placementRefs.current[key]) placementRefs.current[key] = createRoot(placement()!)
|
|
123
|
-
const root = placementRefs.current[key]!
|
|
124
|
-
root.render(
|
|
125
|
-
<RecommendationComponentWrapper
|
|
126
|
-
nostoRecommendation={recommendation}
|
|
127
|
-
></RecommendationComponentWrapper>
|
|
128
|
-
)
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return { renderCampaigns, pageTypeUpdated }
|
|
134
|
-
}
|
|
135
|
-
|
|
136
82
|
useEffect(() => {
|
|
137
83
|
if (!window.nostojs) {
|
|
138
84
|
window.nostojs = (cb: (api: NostoClient) => void) => {
|
|
@@ -141,7 +87,7 @@ export default function NostoProvider(props: NostoProviderProps) {
|
|
|
141
87
|
window.nostojs(api => api.setAutoLoad(false))
|
|
142
88
|
}
|
|
143
89
|
|
|
144
|
-
if (!
|
|
90
|
+
if (!isNostoLoaded() && !shopifyMarkets) {
|
|
145
91
|
const script = document.createElement("script")
|
|
146
92
|
script.type = "text/javascript"
|
|
147
93
|
script.src = "//" + (host || "connect.nosto.com") + "/include/" + account
|
|
@@ -209,9 +155,7 @@ export default function NostoProvider(props: NostoProviderProps) {
|
|
|
209
155
|
clientScriptLoaded,
|
|
210
156
|
currentVariation,
|
|
211
157
|
responseMode,
|
|
212
|
-
recommendationComponent
|
|
213
|
-
useRenderCampaigns,
|
|
214
|
-
pageType,
|
|
158
|
+
recommendationComponent
|
|
215
159
|
}}
|
|
216
160
|
>
|
|
217
161
|
{children}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useNostoApi } from "../utils/hooks"
|
|
1
|
+
import { useRenderCampaigns, useNostoApi } from "../hooks"
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* You can personalise your search pages by using the NostoSearch component.
|
|
@@ -26,9 +25,7 @@ import { useNostoApi } from "../utils/hooks"
|
|
|
26
25
|
*/
|
|
27
26
|
export default function NostoSearch(props: { query: string; placements?: string[] }) {
|
|
28
27
|
const { query, placements } = props
|
|
29
|
-
const {
|
|
30
|
-
|
|
31
|
-
const { renderCampaigns, pageTypeUpdated } = useRenderCampaigns("search")
|
|
28
|
+
const { renderCampaigns } = useRenderCampaigns()
|
|
32
29
|
|
|
33
30
|
useNostoApi(
|
|
34
31
|
async (api) => {
|
|
@@ -36,19 +33,9 @@ export default function NostoSearch(props: { query: string; placements?: string[
|
|
|
36
33
|
.viewSearch(query)
|
|
37
34
|
.setPlacements(placements || api.placements.getPlacements())
|
|
38
35
|
.load()
|
|
39
|
-
renderCampaigns(data
|
|
36
|
+
renderCampaigns(data)
|
|
40
37
|
},
|
|
41
|
-
[query
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<>
|
|
46
|
-
<div className="nosto_page_type" style={{ display: "none" }}>
|
|
47
|
-
search
|
|
48
|
-
</div>
|
|
49
|
-
<div className="nosto_search" style={{ display: "none" }}>
|
|
50
|
-
{query}
|
|
51
|
-
</div>
|
|
52
|
-
</>
|
|
38
|
+
[query]
|
|
53
39
|
)
|
|
40
|
+
return null
|
|
54
41
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { useNostoContext } from "
|
|
1
|
+
import { useNostoContext, useDeepCompareEffect } from "../hooks"
|
|
2
2
|
import { Cart, Customer } from "../types"
|
|
3
3
|
import { snakeize } from "../utils/snakeize"
|
|
4
|
-
import { useDeepCompareEffect } from "../utils/hooks"
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Nosto React requires that you pass it the details of current cart contents and the details of the currently logged-in customer, if any, on every route change.
|
|
@@ -13,8 +12,8 @@ import { useDeepCompareEffect } from "../utils/hooks"
|
|
|
13
12
|
*
|
|
14
13
|
* @group Essential Functions
|
|
15
14
|
*/
|
|
16
|
-
export default function NostoSession(props
|
|
17
|
-
const { cart, customer } = props
|
|
15
|
+
export default function NostoSession(props?: { cart?: Cart; customer?: Customer }) {
|
|
16
|
+
const { cart, customer } = props ?? {}
|
|
18
17
|
const { clientScriptLoaded } = useNostoContext()
|
|
19
18
|
|
|
20
19
|
useDeepCompareEffect(() => {
|
package/src/components/index.ts
CHANGED
|
@@ -8,7 +8,4 @@ export { default as NostoOrder } from "./NostoOrder"
|
|
|
8
8
|
export { default as NostoHome } from "./NostoHome"
|
|
9
9
|
export { default as NostoPlacement } from "./NostoPlacement"
|
|
10
10
|
export { default as NostoProvider } from "./NostoProvider"
|
|
11
|
-
|
|
12
|
-
export { NostoContext, useNostoContext } from "./context"
|
|
13
|
-
export type { NostoContextType } from "./context"
|
|
14
11
|
export { default as NostoSession } from "./NostoSession"
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createContext } from "react"
|
|
2
|
+
import { Recommendation, RenderMode } from "./types"
|
|
3
|
+
|
|
4
|
+
type AnyFunction = (...args: unknown[]) => unknown
|
|
5
|
+
|
|
6
|
+
export type RecommendationComponent = React.ReactElement<{
|
|
7
|
+
nostoRecommendation: Recommendation
|
|
8
|
+
}>
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @group Types
|
|
12
|
+
*/
|
|
13
|
+
export interface NostoContextType {
|
|
14
|
+
account: string
|
|
15
|
+
clientScriptLoaded: boolean
|
|
16
|
+
currentVariation?: string
|
|
17
|
+
renderFunction?: AnyFunction
|
|
18
|
+
responseMode: RenderMode
|
|
19
|
+
recommendationComponent?: RecommendationComponent
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @group Essential Functions
|
|
24
|
+
*/
|
|
25
|
+
export const NostoContext = createContext<NostoContextType>({
|
|
26
|
+
account: "",
|
|
27
|
+
currentVariation: "",
|
|
28
|
+
responseMode: "HTML",
|
|
29
|
+
clientScriptLoaded: false
|
|
30
|
+
})
|
|
31
|
+
|
|
@@ -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,3 +1,4 @@
|
|
|
1
|
-
export type {
|
|
2
|
-
|
|
1
|
+
export type { Cart, Customer, Product, Order, Recommendation } from "./types"
|
|
3
2
|
export * from "./components"
|
|
3
|
+
export { NostoContext, type NostoContextType } from "./context"
|
|
4
|
+
export { useNostoContext } from "./hooks/useNostoContext"
|