@usereactify/search 3.2.0 → 3.4.1

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 CHANGED
@@ -38,4 +38,9 @@ npm run storybook
38
38
 
39
39
  # release a version
40
40
  npm run release
41
+
42
+ # release a beta version
43
+ npm run release -- --prerelease beta
44
+
45
+ npm publish --tag beta
41
46
  ```
@@ -5,8 +5,10 @@ export * from "./useConfig";
5
5
  export * from "./useSearch";
6
6
  export * from "./useFilters";
7
7
  export * from "./useCuration";
8
+ export * from "./useAnalytics";
8
9
  export * from "./useCollection";
9
10
  export * from "./useFilterStack";
10
11
  export * from "./useProductPrice";
11
12
  export * from "./useFilterListProps";
12
13
  export * from "./useFilterCollapsedState";
14
+ export * from "./useLiveConfig";
@@ -17,8 +17,10 @@ __exportStar(require("./useConfig"), exports);
17
17
  __exportStar(require("./useSearch"), exports);
18
18
  __exportStar(require("./useFilters"), exports);
19
19
  __exportStar(require("./useCuration"), exports);
20
+ __exportStar(require("./useAnalytics"), exports);
20
21
  __exportStar(require("./useCollection"), exports);
21
22
  __exportStar(require("./useFilterStack"), exports);
22
23
  __exportStar(require("./useProductPrice"), exports);
23
24
  __exportStar(require("./useFilterListProps"), exports);
24
25
  __exportStar(require("./useFilterCollapsedState"), exports);
26
+ __exportStar(require("./useLiveConfig"), exports);
@@ -33,8 +33,15 @@ const useReactiveDataSearchProps = (props = {}) => {
33
33
  return;
34
34
  submitSearch();
35
35
  }, [submitSearch]);
36
+ const { track } = (0, __1.useAnalytics)();
37
+ const trackQuery = react_1.default.useCallback(() => {
38
+ if (!searchQuery)
39
+ return;
40
+ track({ eventName: "search", payload: { searchTerm: searchQuery } });
41
+ }, [searchQuery, track]);
36
42
  const { run: runDebouncedTriggerQuery, cancel: cancelDebouncedTriggerQuery } = (0, ahooks_1.useDebounceFn)((triggerQuery) => {
37
43
  triggerQuery();
44
+ trackQuery();
38
45
  }, {
39
46
  wait: debounce !== null && debounce !== void 0 ? debounce : 300,
40
47
  });
@@ -0,0 +1,28 @@
1
+ export declare const useAnalytics: () => {
2
+ track: (event: TrackEvent) => Promise<import("axios").AxiosResponse<any, any> | undefined>;
3
+ };
4
+ export declare type TrackEvent = TrackEvent.SearchEvent | TrackEvent.ClickProductEvent;
5
+ export declare namespace TrackEvent {
6
+ interface SearchEvent {
7
+ eventName: "search";
8
+ payload: SearchEvent.Payload;
9
+ }
10
+ namespace SearchEvent {
11
+ interface Payload {
12
+ searchTerm: string;
13
+ }
14
+ }
15
+ interface ClickProductEvent {
16
+ eventName: "clickProduct";
17
+ payload: ClickProductEvent.Payload;
18
+ }
19
+ namespace ClickProductEvent {
20
+ interface Payload {
21
+ elasticProduct: ElasticProduct;
22
+ }
23
+ }
24
+ interface ElasticProduct {
25
+ id: number;
26
+ title: string;
27
+ }
28
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.useAnalytics = void 0;
16
+ const axios_1 = __importDefault(require("axios"));
17
+ const provider_1 = require("../provider");
18
+ const useAnalytics = () => {
19
+ const { shopifyPermanentDomain } = (0, provider_1.useContext)();
20
+ const url = "https://analytics.search.reactify.app/record/";
21
+ const track = (event) => __awaiter(void 0, void 0, void 0, function* () {
22
+ if (!shopifyPermanentDomain) {
23
+ console.warn(new Error('Unable to send tracking event, missing value for "shopifyPermanentDomain".'));
24
+ return;
25
+ }
26
+ const events = getTrackEvents(event);
27
+ return axios_1.default.post(url, { events }, { headers: { "X-Reactify-Tenant": shopifyPermanentDomain } });
28
+ });
29
+ return { track };
30
+ };
31
+ exports.useAnalytics = useAnalytics;
32
+ function getTrackEvents(event) {
33
+ const { eventName } = event;
34
+ let events = [];
35
+ switch (eventName) {
36
+ case "search":
37
+ events = [
38
+ {
39
+ eventName,
40
+ payload: event.payload,
41
+ },
42
+ ];
43
+ break;
44
+ case "clickProduct":
45
+ events = [
46
+ {
47
+ eventName,
48
+ payload: event.payload,
49
+ },
50
+ ];
51
+ break;
52
+ }
53
+ return events;
54
+ }
@@ -0,0 +1,5 @@
1
+ import { Config } from "../types/config";
2
+ export declare const useLiveConfig: (shopifyPermanentDomain: string) => {
3
+ config: Config;
4
+ configLoading: boolean;
5
+ };
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.useLiveConfig = void 0;
16
+ const react_1 = __importDefault(require("react"));
17
+ const debug = require("debug")("reactify-search:useLiveConfig");
18
+ // 5 minute cache expiry
19
+ const CACHE_EXPIRY = 5 * 60 * 1000;
20
+ const useLiveConfig = (shopifyPermanentDomain) => {
21
+ // synchronously returns cached and non-expired config from session storage
22
+ const cachedConfig = react_1.default.useMemo(() => {
23
+ var _a;
24
+ // skip checking cache if url search param "nocache" is set
25
+ const skipCache = new URLSearchParams(window.location.href.split("?")[1]).get("nocache") !== null;
26
+ if (skipCache) {
27
+ debug("skipping cache");
28
+ return;
29
+ }
30
+ const sessionConfig = JSON.parse((_a = window.sessionStorage.getItem("reactify-search:config")) !== null && _a !== void 0 ? _a : "null");
31
+ if (sessionConfig) {
32
+ const sessionConfigTtl = sessionConfig.expiresAt - Date.now();
33
+ debug(`found ${sessionConfigTtl > 0 ? "cached" : "expired"} config`);
34
+ if (sessionConfigTtl > 0) {
35
+ debug(`config expires in ${(sessionConfigTtl / 1000).toFixed()} seconds`);
36
+ return sessionConfig.config;
37
+ }
38
+ }
39
+ return;
40
+ }, []);
41
+ const [config, setConfig] = react_1.default.useState(cachedConfig !== null && cachedConfig !== void 0 ? cachedConfig : {
42
+ fields: [],
43
+ filters: [],
44
+ sort: [],
45
+ curations: [],
46
+ redirects: [],
47
+ });
48
+ const [configLoading, setConfigLoading] = react_1.default.useState(cachedConfig ? false : true);
49
+ react_1.default.useEffect(() => {
50
+ // fetch remote config if no cached config is found
51
+ if (!cachedConfig) {
52
+ (() => __awaiter(void 0, void 0, void 0, function* () {
53
+ debug("fetching fresh config");
54
+ setConfigLoading(true);
55
+ const json = yield fetch(`https://config.search.reactify.app/?shop=${shopifyPermanentDomain}`).then((response) => response.json());
56
+ setConfig(json.body);
57
+ setConfigLoading(false);
58
+ window.sessionStorage.setItem("reactify-search:config", JSON.stringify({
59
+ expiresAt: new Date().getTime() + CACHE_EXPIRY,
60
+ config: json.body,
61
+ }));
62
+ }))();
63
+ }
64
+ }, [shopifyPermanentDomain]);
65
+ return {
66
+ config,
67
+ configLoading,
68
+ };
69
+ };
70
+ exports.useLiveConfig = useLiveConfig;
@@ -1,5 +1,5 @@
1
1
  export declare const useSort: () => {
2
- sortOption: import("..").ConfigSort;
2
+ sortOption: import("..").ConfigSort | undefined;
3
3
  sortOptions: import("..").ConfigSort[];
4
4
  setSortOption: (sortOptionId: string) => void;
5
5
  };
@@ -3,7 +3,9 @@ import type { Config, ConfigSort, ConfigFilter, ConfigCuration } from "./types/c
3
3
  declare type Context = {
4
4
  index: string;
5
5
  config: Config;
6
- sortOption: ConfigSort;
6
+ configLoading: boolean;
7
+ shopifyPermanentDomain: string;
8
+ sortOption: ConfigSort | undefined;
7
9
  sortOptions: ConfigSort[];
8
10
  searchQuery?: string;
9
11
  filterStack?: ConfigFilter;
@@ -23,7 +25,7 @@ declare type Context = {
23
25
  declare const Context: React.Context<Context | undefined>;
24
26
  declare type Props = {
25
27
  index: string;
26
- config: Config;
28
+ shopifyPermanentDomain: string;
27
29
  filterStackId?: string;
28
30
  collection?: Collection;
29
31
  noReactiveBase?: boolean;
package/dist/provider.js CHANGED
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.useContext = exports.Provider = void 0;
7
7
  const react_1 = __importDefault(require("react"));
8
8
  const UtilityAuthenticatedReactiveBase_1 = require("./utility/UtilityAuthenticatedReactiveBase");
9
+ const hooks_1 = require("./hooks");
9
10
  const debug = require("debug")("reactify-search:Provider");
10
11
  const Context = react_1.default.createContext(undefined);
11
12
  const defaultCredentials = {
@@ -15,7 +16,7 @@ const defaultCredentials = {
15
16
  };
16
17
  const Provider = (props) => {
17
18
  var _a, _b;
18
- const { index, config, children, collection, instantSearch, filterStackId, noReactiveBase, additionalComponentIds, } = props;
19
+ const { index, shopifyPermanentDomain, children, collection, instantSearch, filterStackId, noReactiveBase, additionalComponentIds, } = props;
19
20
  const credentials = (_a = props.credentials) !== null && _a !== void 0 ? _a : defaultCredentials;
20
21
  const theme = (_b = props.theme) !== null && _b !== void 0 ? _b : {
21
22
  typography: {
@@ -33,6 +34,7 @@ const Provider = (props) => {
33
34
  react_1.default.useEffect(() => {
34
35
  debug("props", props);
35
36
  }, [props]);
37
+ const { config, configLoading } = (0, hooks_1.useLiveConfig)(props.shopifyPermanentDomain);
36
38
  // @todo make this hackable with a prop
37
39
  // https://gitlab.com/reactifyapps/reactify-search-frontend/-/issues/1
38
40
  const searchQueryFromURL = react_1.default.useMemo(() => {
@@ -64,6 +66,8 @@ const Provider = (props) => {
64
66
  const contextValue = react_1.default.useMemo(() => ({
65
67
  index,
66
68
  config,
69
+ configLoading,
70
+ shopifyPermanentDomain,
67
71
  collection,
68
72
  credentials,
69
73
  searchQuery,
@@ -83,6 +87,7 @@ const Provider = (props) => {
83
87
  index,
84
88
  config,
85
89
  curation,
90
+ shopifyPermanentDomain,
86
91
  collection,
87
92
  credentials,
88
93
  searchQuery,
@@ -108,13 +113,14 @@ exports.Provider = Provider;
108
113
  const useContext = () => react_1.default.useContext(Context);
109
114
  exports.useContext = useContext;
110
115
  const useSortState = (config, collection) => {
116
+ var _a;
111
117
  const sortOptions = react_1.default.useMemo(() => {
112
118
  const type = !!collection ? "collection" : "search";
113
119
  return config.sort
114
120
  .sort((a, b) => `${a.position}`.localeCompare(`${b.position}`))
115
121
  .filter(({ visibility }) => ["all", type].includes(visibility));
116
122
  }, [config, collection]);
117
- const [sortOptionState, setSortOptionState] = react_1.default.useState(sortOptions[0].id);
123
+ const [sortOptionState, setSortOptionState] = react_1.default.useState((_a = sortOptions[0]) === null || _a === void 0 ? void 0 : _a.id);
118
124
  const sortOption = react_1.default.useMemo(() => sortOptions.find(({ id }) => id === sortOptionState) || sortOptions[0], [sortOptions, sortOptionState]);
119
125
  const setSortOption = react_1.default.useCallback((sortOptionId) => setSortOptionState(sortOptionId), []);
120
126
  return react_1.default.useMemo(() => ({ sortOptions, sortOption, setSortOption }), [sortOption, sortOptions, setSortOption]);
@@ -24,8 +24,20 @@ const ResultCard = (_a) => {
24
24
  if (render)
25
25
  return render(Object.assign(Object.assign({}, props), productPrice));
26
26
  const { formattedPrice, formattedCompareAtPrice, onSale } = productPrice;
27
+ const { track } = (0, hooks_1.useAnalytics)();
28
+ const handleClick = react_1.default.useCallback(() => {
29
+ track({
30
+ eventName: "clickProduct",
31
+ payload: {
32
+ elasticProduct: {
33
+ id: product.id,
34
+ title: product.title,
35
+ },
36
+ },
37
+ });
38
+ }, [track, product]);
27
39
  return (react_1.default.createElement("article", null,
28
- react_1.default.createElement("a", { href: `/products/${product.handle}` },
40
+ react_1.default.createElement("a", { onClick: handleClick, href: `/products/${product.handle}` },
29
41
  product.image && react_1.default.createElement("img", { src: product.image, width: "100%" }),
30
42
  product.title),
31
43
  react_1.default.createElement("span", null, formattedPrice),
@@ -14,6 +14,7 @@ declare type Props = {
14
14
  pageSize?: number;
15
15
  gridColumns?: number;
16
16
  listClassName?: string;
17
+ renderBooting?: () => JSX.Element | null;
17
18
  renderLoading?: () => JSX.Element | null;
18
19
  renderNoResults?: () => JSX.Element | null;
19
20
  renderResultCard?: Parameters<typeof ResultCard>[0]["render"];
@@ -11,6 +11,7 @@ const ResultPagination_1 = require("./ResultPagination");
11
11
  const ResultCardCallout_1 = require("./ResultCardCallout");
12
12
  const ResultLoadMoreButton_1 = require("./ResultLoadMoreButton");
13
13
  const ResultPaginationNextPrev_1 = require("./ResultPaginationNextPrev");
14
+ const provider_1 = require("../provider");
14
15
  const hooks_1 = require("../hooks");
15
16
  const elastic_1 = require("../types/elastic");
16
17
  const ResultList = (props) => {
@@ -22,9 +23,10 @@ const ResultList = (props) => {
22
23
  };
23
24
  exports.ResultList = ResultList;
24
25
  const ResultListInner = (props) => {
25
- const { gridColumns, renderError, renderAfter, renderBefore, renderLoading, listClassName, renderResults, renderNoResults, renderResultCard, renderLoadMoreButton, renderResultCardCallout, reactivesearchResultProps, } = props;
26
+ const { gridColumns, renderError, renderAfter, renderBefore, renderBooting, renderLoading, listClassName, renderResults, renderNoResults, renderResultCard, renderLoadMoreButton, renderResultCardCallout, reactivesearchResultProps, } = props;
26
27
  const filterStack = (0, hooks_1.useFilterStack)();
27
28
  const { instantSearch } = (0, hooks_1.useSearch)();
29
+ const { configLoading } = (0, provider_1.useContext)();
28
30
  const initialSearchHasRun = react_1.default.useMemo(() => "undefined" !==
29
31
  typeof reactivesearchResultProps.resultStats.numberOfResults, [reactivesearchResultProps]);
30
32
  const styleProp = react_1.default.useMemo(() => ({
@@ -32,6 +34,11 @@ const ResultListInner = (props) => {
32
34
  gridTemplateColumns: `repeat(${gridColumns !== null && gridColumns !== void 0 ? gridColumns : 4}, minmax(0, 1fr))`,
33
35
  }), [gridColumns]);
34
36
  const resultProps = react_1.default.useMemo(() => (Object.assign(Object.assign({}, reactivesearchResultProps), { products: reactivesearchResultProps.data.filter((document) => elastic_1.ElasticDocumentType.Product === document.type || !document.type), callouts: reactivesearchResultProps.data.filter((document) => elastic_1.ElasticDocumentType.Callout === document.type) })), [reactivesearchResultProps]);
37
+ if (configLoading) {
38
+ if (renderBooting)
39
+ return renderBooting();
40
+ return react_1.default.createElement("div", null, "Loading...");
41
+ }
35
42
  if (reactivesearchResultProps.error) {
36
43
  if (renderError)
37
44
  return renderError({ error: reactivesearchResultProps.error });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@usereactify/search",
3
3
  "description": "React UI library for Reactify Search",
4
- "version": "3.2.0",
4
+ "version": "3.4.1",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -30,6 +30,7 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "ahooks": "2.10.11",
33
+ "axios": "0.26.1",
33
34
  "currency.js": "2.0.4",
34
35
  "debug": "4.3.2"
35
36
  },