@nosto/nosto-react 2.0.0 → 2.2.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 +13 -0
- package/dist/index.d.ts +172 -27
- package/dist/index.es.js +552 -521
- package/dist/index.umd.js +9 -9
- package/package.json +8 -19
- package/src/components/Nosto404.tsx +17 -3
- package/src/components/NostoCategory.tsx +19 -3
- package/src/components/NostoCheckout.tsx +17 -3
- package/src/components/NostoHome.tsx +17 -3
- package/src/components/NostoOrder.tsx +19 -6
- package/src/components/NostoOther.tsx +17 -3
- package/src/components/NostoPlacement.tsx +5 -3
- package/src/components/NostoProduct.tsx +21 -8
- package/src/components/NostoProvider.tsx +21 -77
- package/src/components/NostoSearch.tsx +19 -3
- package/src/components/NostoSession.tsx +26 -6
- package/src/components/index.ts +11 -11
- package/src/context.ts +2 -2
- package/src/hooks/index.ts +2 -1
- package/src/hooks/scriptLoader.ts +30 -0
- package/src/hooks/useLoadClientScript.ts +84 -0
- package/src/hooks/useNostoApi.ts +2 -2
- package/src/hooks/useNostoContext.ts +1 -7
- package/src/hooks/useRenderCampaigns.tsx +2 -3
- package/src/index.ts +1 -0
- package/src/utils/snakeize.ts +5 -1
- package/src/utils/types.ts +29 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { isValidElement } from "react"
|
|
2
2
|
import { NostoContext, RecommendationComponent } from "../context"
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { useLoadClientScript } from "../hooks"
|
|
4
|
+
import type { ReactElement } from "react"
|
|
5
|
+
import { ScriptLoadOptions } from "../hooks/scriptLoader"
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @group Components
|
|
@@ -22,7 +23,7 @@ export interface NostoProviderProps {
|
|
|
22
23
|
/**
|
|
23
24
|
* children
|
|
24
25
|
*/
|
|
25
|
-
children:
|
|
26
|
+
children: ReactElement | ReactElement[]
|
|
26
27
|
/**
|
|
27
28
|
* Indicates if merchant uses multiple currencies
|
|
28
29
|
*/
|
|
@@ -38,6 +39,14 @@ export interface NostoProviderProps {
|
|
|
38
39
|
language?: string
|
|
39
40
|
marketId?: string | number
|
|
40
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Load nosto script (should be false if loading the script outside of nosto-react)
|
|
44
|
+
*/
|
|
45
|
+
loadScript?: boolean
|
|
46
|
+
/**
|
|
47
|
+
* Custom script loader
|
|
48
|
+
*/
|
|
49
|
+
scriptLoader?: (scriptSrc: string, options?: ScriptLoadOptions) => Promise<void>
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
/**
|
|
@@ -65,88 +74,23 @@ export default function NostoProvider(props: NostoProviderProps) {
|
|
|
65
74
|
const {
|
|
66
75
|
account,
|
|
67
76
|
multiCurrency = false,
|
|
68
|
-
host,
|
|
69
77
|
children,
|
|
70
78
|
recommendationComponent,
|
|
71
|
-
shopifyMarkets
|
|
72
79
|
} = props
|
|
73
|
-
const [clientScriptLoadedState, setClientScriptLoadedState] = React.useState(false)
|
|
74
|
-
const clientScriptLoaded = React.useMemo(() => clientScriptLoadedState, [clientScriptLoadedState])
|
|
75
80
|
|
|
76
81
|
// Pass currentVariation as empty string if multiCurrency is disabled
|
|
77
82
|
const currentVariation = multiCurrency ? props.currentVariation : ""
|
|
78
83
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
window.nostojs = (cb: (api: NostoClient) => void) => {
|
|
85
|
-
(window.nostojs.q = window.nostojs.q || []).push(cb)
|
|
86
|
-
}
|
|
87
|
-
window.nostojs(api => api.setAutoLoad(false))
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (!isNostoLoaded() && !shopifyMarkets) {
|
|
91
|
-
const script = document.createElement("script")
|
|
92
|
-
script.type = "text/javascript"
|
|
93
|
-
script.src = "//" + (host || "connect.nosto.com") + "/include/" + account
|
|
94
|
-
script.async = true
|
|
95
|
-
script.setAttribute("nosto-client-script", "")
|
|
96
|
-
|
|
97
|
-
script.onload = () => {
|
|
98
|
-
if (typeof jest !== "undefined") {
|
|
99
|
-
window.nosto?.reload({
|
|
100
|
-
site: "localhost",
|
|
101
|
-
})
|
|
102
|
-
}
|
|
103
|
-
setClientScriptLoadedState(true)
|
|
104
|
-
}
|
|
105
|
-
document.body.appendChild(script)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Enable Shopify markets functionality:
|
|
109
|
-
if (shopifyMarkets) {
|
|
110
|
-
const existingScript = document.querySelector("[nosto-client-script]")
|
|
111
|
-
const nostoSandbox = document.querySelector("#nosto-sandbox")
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
!existingScript ||
|
|
115
|
-
existingScript?.getAttribute("nosto-language") !== shopifyMarkets?.language ||
|
|
116
|
-
existingScript?.getAttribute("nosto-market-id") !== shopifyMarkets?.marketId
|
|
117
|
-
) {
|
|
118
|
-
if (clientScriptLoadedState) {
|
|
119
|
-
setClientScriptLoadedState(false)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
existingScript?.parentNode?.removeChild(existingScript)
|
|
123
|
-
nostoSandbox?.parentNode?.removeChild(nostoSandbox)
|
|
84
|
+
if (recommendationComponent && !isValidElement(recommendationComponent)) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
"The recommendationComponent prop must be a valid React element. Please provide a valid React element."
|
|
87
|
+
)
|
|
88
|
+
}
|
|
124
89
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
script.src =
|
|
128
|
-
"//" +
|
|
129
|
-
(host || "connect.nosto.com") +
|
|
130
|
-
`/script/shopify/market/nosto.js?merchant=${account}&market=${
|
|
131
|
-
shopifyMarkets.marketId || ""
|
|
132
|
-
}&locale=${shopifyMarkets?.language?.toLowerCase() || ""}`
|
|
133
|
-
script.async = true
|
|
134
|
-
script.setAttribute("nosto-client-script", "")
|
|
135
|
-
script.setAttribute("nosto-language", shopifyMarkets?.language || "")
|
|
136
|
-
script.setAttribute("nosto-market-id", String(shopifyMarkets?.marketId))
|
|
90
|
+
// Set responseMode for loading campaigns:
|
|
91
|
+
const responseMode = recommendationComponent ? "JSON_ORIGINAL" : "HTML"
|
|
137
92
|
|
|
138
|
-
|
|
139
|
-
if (typeof jest !== "undefined") {
|
|
140
|
-
window.nosto?.reload({
|
|
141
|
-
site: "localhost",
|
|
142
|
-
})
|
|
143
|
-
}
|
|
144
|
-
setClientScriptLoadedState(true)
|
|
145
|
-
}
|
|
146
|
-
document.body.appendChild(script)
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}, [clientScriptLoadedState, shopifyMarkets])
|
|
93
|
+
const { clientScriptLoaded } = useLoadClientScript(props)
|
|
150
94
|
|
|
151
95
|
return (
|
|
152
96
|
<NostoContext.Provider
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { useRenderCampaigns, useNostoApi } from "../hooks"
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @group Components
|
|
5
|
+
*/
|
|
6
|
+
export type NostoSearchProps = {
|
|
7
|
+
query: string
|
|
8
|
+
placements?: string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
/**
|
|
4
12
|
* You can personalise your search pages by using the NostoSearch component.
|
|
5
13
|
* The component requires that you provide it the current search term.
|
|
@@ -23,8 +31,17 @@ import { useRenderCampaigns, useNostoApi } from "../hooks"
|
|
|
23
31
|
*
|
|
24
32
|
* @group Components
|
|
25
33
|
*/
|
|
26
|
-
export default function NostoSearch(props:
|
|
27
|
-
|
|
34
|
+
export default function NostoSearch(props: NostoSearchProps) {
|
|
35
|
+
useNostoSearch(props)
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* You can personalise your search pages by using the useNostoSearch hook.
|
|
41
|
+
*
|
|
42
|
+
* @group Hooks
|
|
43
|
+
*/
|
|
44
|
+
export function useNostoSearch({ query, placements }: NostoSearchProps) {
|
|
28
45
|
const { renderCampaigns } = useRenderCampaigns()
|
|
29
46
|
|
|
30
47
|
useNostoApi(
|
|
@@ -37,5 +54,4 @@ export default function NostoSearch(props: { query: string; placements?: string[
|
|
|
37
54
|
},
|
|
38
55
|
[query]
|
|
39
56
|
)
|
|
40
|
-
return null
|
|
41
57
|
}
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { useNostoContext, useDeepCompareEffect } from "../hooks"
|
|
2
|
-
import { Cart, Customer } from "../types"
|
|
2
|
+
import { Cart as CartSnakeCase, Customer as CustomerSnakeCase } from "../types"
|
|
3
3
|
import { snakeize } from "../utils/snakeize"
|
|
4
|
+
import { ToCamelCase } from "../utils/types"
|
|
5
|
+
|
|
6
|
+
type Cart = CartSnakeCase | ToCamelCase<CartSnakeCase>
|
|
7
|
+
type Customer = CustomerSnakeCase | ToCamelCase<CustomerSnakeCase>
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @group Components
|
|
11
|
+
*/
|
|
12
|
+
export type NostoSessionProps = {
|
|
13
|
+
cart?: Cart
|
|
14
|
+
customer?: Customer
|
|
15
|
+
}
|
|
4
16
|
|
|
5
17
|
/**
|
|
6
18
|
* 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.
|
|
@@ -10,10 +22,19 @@ import { snakeize } from "../utils/snakeize"
|
|
|
10
22
|
*
|
|
11
23
|
* The cart prop requires a value that adheres to the type `Cart`, while the customer prop requires a value that adheres to the type `Customer`.
|
|
12
24
|
*
|
|
13
|
-
* @group
|
|
25
|
+
* @group Components
|
|
26
|
+
*/
|
|
27
|
+
export default function NostoSession(props?: NostoSessionProps) {
|
|
28
|
+
useNostoSession(props)
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 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.
|
|
34
|
+
*
|
|
35
|
+
* @group Hooks
|
|
14
36
|
*/
|
|
15
|
-
export
|
|
16
|
-
const { cart, customer } = props ?? {}
|
|
37
|
+
export function useNostoSession({ cart, customer }: NostoSessionProps = {}) {
|
|
17
38
|
const { clientScriptLoaded } = useNostoContext()
|
|
18
39
|
|
|
19
40
|
useDeepCompareEffect(() => {
|
|
@@ -28,10 +49,9 @@ export default function NostoSession(props?: { cart?: Cart; customer?: Customer
|
|
|
28
49
|
.setCart(currentCart)
|
|
29
50
|
.setCustomer(currentCustomer)
|
|
30
51
|
.viewOther()
|
|
31
|
-
.load()
|
|
52
|
+
.load({ skipPageViews: true })
|
|
32
53
|
})
|
|
33
54
|
}
|
|
34
55
|
}, [clientScriptLoaded, cart, customer])
|
|
35
56
|
|
|
36
|
-
return <></>
|
|
37
57
|
}
|
package/src/components/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export { default as Nosto404 } from "./Nosto404"
|
|
2
|
-
export { default as NostoOther } from "./NostoOther"
|
|
3
|
-
export { default as NostoCheckout } from "./NostoCheckout"
|
|
4
|
-
export { default as NostoProduct } from "./NostoProduct"
|
|
5
|
-
export { default as NostoCategory } from "./NostoCategory"
|
|
6
|
-
export { default as NostoSearch } from "./NostoSearch"
|
|
7
|
-
export { default as NostoOrder } from "./NostoOrder"
|
|
8
|
-
export { default as NostoHome } from "./NostoHome"
|
|
9
|
-
export { default as NostoPlacement } from "./NostoPlacement"
|
|
10
|
-
export { default as NostoProvider } from "./NostoProvider"
|
|
11
|
-
export { default as NostoSession } from "./NostoSession"
|
|
1
|
+
export { default as Nosto404, useNosto404, type Nosto404Props } from "./Nosto404"
|
|
2
|
+
export { default as NostoOther, useNostoOther, type NostoOtherProps } from "./NostoOther"
|
|
3
|
+
export { default as NostoCheckout, useNostoCheckout, type NostoCheckoutProps } from "./NostoCheckout"
|
|
4
|
+
export { default as NostoProduct, useNostoProduct, type NostoProductProps } from "./NostoProduct"
|
|
5
|
+
export { default as NostoCategory, useNostoCategory, type NostoCategoryProps } from "./NostoCategory"
|
|
6
|
+
export { default as NostoSearch, useNostoSearch, type NostoSearchProps } from "./NostoSearch"
|
|
7
|
+
export { default as NostoOrder, useNostoOrder, type NostoOrderProps } from "./NostoOrder"
|
|
8
|
+
export { default as NostoHome, useNostoHome, type NostoHomeProps } from "./NostoHome"
|
|
9
|
+
export { default as NostoPlacement, type NostoPlacementProps } from "./NostoPlacement"
|
|
10
|
+
export { default as NostoProvider, type NostoProviderProps } from "./NostoProvider"
|
|
11
|
+
export { default as NostoSession, useNostoSession, type NostoSessionProps } from "./NostoSession"
|
package/src/context.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { createContext } from "react"
|
|
1
|
+
import { createContext, ReactElement } from "react"
|
|
2
2
|
import { Recommendation, RenderMode } from "./types"
|
|
3
3
|
|
|
4
4
|
type AnyFunction = (...args: unknown[]) => unknown
|
|
5
5
|
|
|
6
|
-
export type RecommendationComponent =
|
|
6
|
+
export type RecommendationComponent = ReactElement<{
|
|
7
7
|
nostoRecommendation: Recommendation
|
|
8
8
|
}>
|
|
9
9
|
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { useDeepCompareEffect } from "./useDeepCompareEffect"
|
|
2
2
|
export { useNostoApi } from "./useNostoApi"
|
|
3
3
|
export { useNostoContext } from "./useNostoContext"
|
|
4
|
-
export { useRenderCampaigns } from "./useRenderCampaigns"
|
|
4
|
+
export { useRenderCampaigns } from "./useRenderCampaigns"
|
|
5
|
+
export { useLoadClientScript } from "./useLoadClientScript"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export default function scriptLoader(scriptSrc: string, options?: ScriptLoadOptions): Promise<void> {
|
|
2
|
+
return new Promise((resolve, reject) => {
|
|
3
|
+
const script = document.createElement("script")
|
|
4
|
+
script.type = "text/javascript"
|
|
5
|
+
script.src = scriptSrc
|
|
6
|
+
script.async = true
|
|
7
|
+
script.onload = () => resolve()
|
|
8
|
+
script.onerror = () => reject()
|
|
9
|
+
Object.entries(options?.attributes ?? {}).forEach(([k, v]) => script.setAttribute(k, v))
|
|
10
|
+
if (options?.position === "head") {
|
|
11
|
+
document.head.appendChild(script)
|
|
12
|
+
} else {
|
|
13
|
+
document.body.appendChild(script)
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @group Types
|
|
20
|
+
*/
|
|
21
|
+
export type ScriptLoadOptions = {
|
|
22
|
+
/**
|
|
23
|
+
* Indicates the position of the script, default is "body"
|
|
24
|
+
*/
|
|
25
|
+
position?: "head" | "body"
|
|
26
|
+
/**
|
|
27
|
+
* Indicates the attributes of the script element
|
|
28
|
+
*/
|
|
29
|
+
attributes?: Record<string, string>
|
|
30
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useState, useEffect } from "react"
|
|
2
|
+
import { isNostoLoaded } from "../components/helpers"
|
|
3
|
+
import type { NostoClient } from "../types"
|
|
4
|
+
import type { NostoProviderProps } from "../components/NostoProvider"
|
|
5
|
+
import scriptLoaderFn from "./scriptLoader"
|
|
6
|
+
|
|
7
|
+
type NostoScriptProps = Pick<NostoProviderProps, "account" | "host" | "shopifyMarkets" | "loadScript" | "scriptLoader">
|
|
8
|
+
|
|
9
|
+
export function useLoadClientScript(props: NostoScriptProps) {
|
|
10
|
+
const { host = "connect.nosto.com", scriptLoader = scriptLoaderFn, account, shopifyMarkets, loadScript = true } = props
|
|
11
|
+
const [clientScriptLoaded, setClientScriptLoaded] = useState(false)
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
function scriptOnload() {
|
|
15
|
+
// Override for production scripts to work in unit tests
|
|
16
|
+
if ("nostoReactTest" in window) {
|
|
17
|
+
window.nosto?.reload({
|
|
18
|
+
site: "localhost"
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
setClientScriptLoaded(true)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Create and append script element
|
|
25
|
+
async function injectScriptElement(urlPartial: string, extraAttributes: Record<string, string> = {}) {
|
|
26
|
+
const scriptSrc = `//${host}${urlPartial}`
|
|
27
|
+
const attributes = { "nosto-client-script": "", ...extraAttributes }
|
|
28
|
+
await scriptLoader(scriptSrc, { attributes })
|
|
29
|
+
scriptOnload()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function prepareShopifyMarketsScript() {
|
|
33
|
+
const existingScript = document.querySelector("[nosto-client-script]")
|
|
34
|
+
|
|
35
|
+
const marketId = String(shopifyMarkets?.marketId || "")
|
|
36
|
+
const language = shopifyMarkets?.language || ""
|
|
37
|
+
|
|
38
|
+
const attributeMismatch =
|
|
39
|
+
existingScript?.getAttribute("nosto-language") !== language ||
|
|
40
|
+
existingScript?.getAttribute("nosto-market-id") !== marketId
|
|
41
|
+
|
|
42
|
+
if (!existingScript || attributeMismatch) {
|
|
43
|
+
if (clientScriptLoaded) {
|
|
44
|
+
setClientScriptLoaded(false)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const nostoSandbox = document.querySelector("#nosto-sandbox")
|
|
48
|
+
|
|
49
|
+
existingScript?.parentNode?.removeChild(existingScript)
|
|
50
|
+
nostoSandbox?.parentNode?.removeChild(nostoSandbox)
|
|
51
|
+
|
|
52
|
+
const urlPartial =
|
|
53
|
+
`/script/shopify/market/nosto.js?merchant=${account}&market=${marketId}&locale=${language.toLowerCase()}`
|
|
54
|
+
injectScriptElement(urlPartial, { "nosto-language": language, "nosto-market-id": marketId })
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Load Nosto API stub
|
|
59
|
+
if (!window.nostojs) {
|
|
60
|
+
window.nostojs = (cb: (api: NostoClient) => void) => {
|
|
61
|
+
(window.nostojs.q = window.nostojs.q || []).push(cb)
|
|
62
|
+
}
|
|
63
|
+
window.nostojs(api => api.setAutoLoad(false))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!loadScript) {
|
|
67
|
+
window.nosto ? scriptOnload() : window.nostojs(scriptOnload)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Load Nosto client script if not already loaded externally
|
|
72
|
+
if (!isNostoLoaded() && !shopifyMarkets) {
|
|
73
|
+
const urlPartial = `/include/${account}`
|
|
74
|
+
injectScriptElement(urlPartial)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Load Shopify Markets scripts
|
|
78
|
+
if (shopifyMarkets) {
|
|
79
|
+
prepareShopifyMarketsScript()
|
|
80
|
+
}
|
|
81
|
+
}, [shopifyMarkets?.marketId, shopifyMarkets?.language])
|
|
82
|
+
|
|
83
|
+
return { clientScriptLoaded }
|
|
84
|
+
}
|
package/src/hooks/useNostoApi.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { useEffect } from "react"
|
|
1
|
+
import { DependencyList, useEffect } from "react"
|
|
2
2
|
import { useNostoContext } from "./useNostoContext"
|
|
3
3
|
import { NostoClient } from "../types"
|
|
4
4
|
import { useDeepCompareEffect } from "./useDeepCompareEffect"
|
|
5
5
|
|
|
6
6
|
export function useNostoApi(
|
|
7
7
|
cb: (api: NostoClient) => void,
|
|
8
|
-
deps?:
|
|
8
|
+
deps?: DependencyList,
|
|
9
9
|
flags?: { deep?: boolean }
|
|
10
10
|
): void {
|
|
11
11
|
const { clientScriptLoaded, currentVariation, responseMode } = useNostoContext()
|
|
@@ -7,12 +7,6 @@ import { NostoContext, NostoContextType } from "../context"
|
|
|
7
7
|
* @group Essential Functions
|
|
8
8
|
*/
|
|
9
9
|
export function useNostoContext(): NostoContextType {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (!context) {
|
|
13
|
-
throw new Error("No nosto context found")
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
return context
|
|
10
|
+
return useContext(NostoContext)
|
|
17
11
|
}
|
|
18
12
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { useRef } from "react"
|
|
1
|
+
import { cloneElement, useRef } from "react"
|
|
2
2
|
import { createRoot, Root } from "react-dom/client"
|
|
3
3
|
import { ActionResponse, Recommendation } from "../types"
|
|
4
4
|
import { useNostoContext } from "./useNostoContext"
|
|
5
|
-
import React from "react"
|
|
6
5
|
import { RecommendationComponent } from "../context"
|
|
7
6
|
|
|
8
7
|
// RecommendationComponent for client-side rendering:
|
|
@@ -10,7 +9,7 @@ function RecommendationComponentWrapper(props: {
|
|
|
10
9
|
recommendationComponent: RecommendationComponent,
|
|
11
10
|
nostoRecommendation: Recommendation }) {
|
|
12
11
|
|
|
13
|
-
return
|
|
12
|
+
return cloneElement(props.recommendationComponent, {
|
|
14
13
|
// eslint-disable-next-line react/prop-types
|
|
15
14
|
nostoRecommendation: props.nostoRecommendation,
|
|
16
15
|
})
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type { Cart, Customer, Product, Order, Recommendation } from "./types"
|
|
2
2
|
export * from "./components"
|
|
3
|
+
export { type ScriptLoadOptions } from "./hooks/scriptLoader"
|
|
3
4
|
export { NostoContext, type NostoContextType } from "./context"
|
|
4
5
|
export { useNostoContext } from "./hooks/useNostoContext"
|
package/src/utils/snakeize.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import { ToSnakeCase } from "./types"
|
|
2
|
+
|
|
3
|
+
// signature override
|
|
4
|
+
export function snakeize<T>(obj: T): ToSnakeCase<T>
|
|
5
|
+
export function snakeize<T>(obj: T): unknown {
|
|
2
6
|
if (!obj || typeof obj !== "object") {
|
|
3
7
|
return obj
|
|
4
8
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
type SnakeToCamelCase<S extends string> = S extends `${infer T}_${infer U}`
|
|
2
|
+
? `${T}${Capitalize<SnakeToCamelCase<U>>}`
|
|
3
|
+
: S
|
|
4
|
+
|
|
5
|
+
// Recursive type to apply the conversion to all keys in an object type
|
|
6
|
+
export type ToCamelCase<T> = T extends (infer U)[]
|
|
7
|
+
? ToCamelCase<U>[]
|
|
8
|
+
: T extends Date ? T
|
|
9
|
+
: T extends object
|
|
10
|
+
? {
|
|
11
|
+
[K in keyof T as SnakeToCamelCase<K & string>]: ToCamelCase<T[K]>
|
|
12
|
+
}
|
|
13
|
+
: T
|
|
14
|
+
|
|
15
|
+
type CamelToSnakeCase<S extends string> = S extends `${infer T}${infer U}`
|
|
16
|
+
? U extends Uncapitalize<U>
|
|
17
|
+
? `${Lowercase<T>}${CamelToSnakeCase<U>}`
|
|
18
|
+
: `${Lowercase<T>}_${CamelToSnakeCase<Uncapitalize<U>>}`
|
|
19
|
+
: S
|
|
20
|
+
|
|
21
|
+
// Recursive type to apply the conversion to all keys in an object type
|
|
22
|
+
export type ToSnakeCase<T> = T extends (infer U)[]
|
|
23
|
+
? ToSnakeCase<U>[]
|
|
24
|
+
: T extends Date ? T
|
|
25
|
+
: T extends object
|
|
26
|
+
? {
|
|
27
|
+
[K in keyof T as CamelToSnakeCase<K & string>]: ToSnakeCase<T[K]>
|
|
28
|
+
}
|
|
29
|
+
: T
|