@findlane/browser 0.10.0 → 0.11.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 CHANGED
@@ -42,7 +42,7 @@ import "@findlane/browser/react/styles.css" // optional
42
42
  <SearchBox publicId="pub_your_public_id" getHref={(hit) => hit.record.url} action="/search" />
43
43
  ```
44
44
 
45
- `SearchDialog` is the same search box in a dialog that ⌘K opens, as on docs sites. A results page shares its state through `SearchProvider`, with `RefinementList`, `SortSelect`, `Hits`, `LoadMore`, and hooks such as `useRefinementList` and `useRange` for your own markup. With `routing`, the search is kept in the URL. `@findlane/browser/react/ssr` renders the first results on the server. Docs: https://findlane.dev/docs/react
45
+ `SearchDialog` is the same search box in a dialog that ⌘K opens, as on docs sites. A results page shares its state through `SearchProvider`, with `RefinementList`, `SortSelect`, `Hits`, `LoadMore`, and hooks such as `useRefinementList` and `useRange` for your own markup. With `routing`, the search is kept in the URL. `@findlane/browser/react/ssr` renders the first results on the server. On a product page, `useProductView(objectID)`, or `<ProductView>` in a server component, sends the page's view for the index's Events page. Docs: https://findlane.dev/docs/react
46
46
 
47
47
  ## Search requests
48
48
 
package/dist/index.d.ts CHANGED
@@ -149,6 +149,8 @@ export type ClickDetails = {
149
149
  /** Events from the page, sent in the background: a batch a second, and what's left when the page is
150
150
  * hidden or closed. They carry no cookie or ID. */
151
151
  export type BrowserEvents = {
152
+ /** A product's page was viewed. A repeat of the last view within 5 seconds is the same page view
153
+ * (React's double mount in development, a re-render), so it isn't sent. */
152
154
  view(objectID: string): void;
153
155
  click(objectID: string, details?: ClickDetails): void;
154
156
  addToCart(items: EventItem[], options?: {
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ function eventSender(url, fetcher, mode) {
13
13
  let allowed = mode === "afterConsent" ? undefined : mode;
14
14
  let timer;
15
15
  let listening = false;
16
+ let lastView = { objectID: "", at: 0 };
16
17
  const flush = () => {
17
18
  clearTimeout(timer);
18
19
  timer = undefined;
@@ -37,7 +38,13 @@ function eventSender(url, fetcher, mode) {
37
38
  timer ??= setTimeout(flush, 1000);
38
39
  };
39
40
  return {
40
- view: (objectID) => add({ type: "view", objectID }),
41
+ view(objectID) {
42
+ const now = Date.now();
43
+ if (objectID === lastView.objectID && now - lastView.at < 5000)
44
+ return;
45
+ lastView = { objectID, at: now };
46
+ add({ type: "view", objectID });
47
+ },
41
48
  click: (objectID, details) => add({ type: "click", objectID, ...details }),
42
49
  addToCart: (items, options) => add({ type: "addToCart", items, ...options }),
43
50
  purchase: (order) => add({ type: "purchase", ...order }),
@@ -3,6 +3,7 @@ export { useSearch, type UseSearchOptions, type UseSearchResult } from "./use-se
3
3
  export { useAutocomplete, type AutocompleteOptions } from "./use-autocomplete.js";
4
4
  export { useRecommendations, type UseRecommendationsOptions, type UseRecommendationsResult } from "./use-recommendations.js";
5
5
  export { Recommendations, type RecommendationsProps } from "./recommendations.js";
6
+ export { ProductView, useProductView } from "./product-view.js";
6
7
  export { SearchProvider, useCurrentRefinements, useDidYouMean, useGeoSearch, useHits, useLoadMore, usePagination, useRange, useRefinementList, useSearchBox, useSearchState, useSortBy, useToggle, type Range, type Refinement, type SearchProviderProps, } from "./search-provider.js";
7
8
  export { CurrentRefinements, DefaultHit, DidYouMean, Hits, LoadMore, RefinementList, SearchBox, SearchInput, SortSelect, Stats, type HitsProps, type RefinementListProps, type SearchBoxProps, } from "./components.js";
8
9
  export { SearchDialog, type SearchDialogProps } from "./dialog.js";
@@ -5,6 +5,7 @@ export { useSearch } from "./use-search.js";
5
5
  export { useAutocomplete } from "./use-autocomplete.js";
6
6
  export { useRecommendations } from "./use-recommendations.js";
7
7
  export { Recommendations } from "./recommendations.js";
8
+ export { ProductView, useProductView } from "./product-view.js";
8
9
  export { SearchProvider, useCurrentRefinements, useDidYouMean, useGeoSearch, useHits, useLoadMore, usePagination, useRange, useRefinementList, useSearchBox, useSearchState, useSortBy, useToggle, } from "./search-provider.js";
9
10
  export { CurrentRefinements, DefaultHit, DidYouMean, Hits, LoadMore, RefinementList, SearchBox, SearchInput, SortSelect, Stats, } from "./components.js";
10
11
  export { SearchDialog } from "./dialog.js";
@@ -0,0 +1,10 @@
1
+ import { type ClientOptions } from "./client.js";
2
+ /** Sends a view of the product when its page shows, and again when `objectID` changes, for the index's
3
+ * Events page and popularity. Nothing is sent while `objectID` is empty. The client's `events` option
4
+ * turns views off, or holds them until consent. */
5
+ export declare function useProductView(objectID: string | null | undefined, options?: ClientOptions): void;
6
+ /** useProductView as a component that renders nothing, for pages that can't call hooks, such as a
7
+ * Next.js server component. */
8
+ export declare function ProductView({ objectID, ...options }: ClientOptions & {
9
+ objectID: string;
10
+ }): null;
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { useEffect } from "react";
3
+ import { useClient } from "./client.js";
4
+ /** Sends a view of the product when its page shows, and again when `objectID` changes, for the index's
5
+ * Events page and popularity. Nothing is sent while `objectID` is empty. The client's `events` option
6
+ * turns views off, or holds them until consent. */
7
+ export function useProductView(objectID, options = {}) {
8
+ const client = useClient(options);
9
+ useEffect(() => {
10
+ if (objectID)
11
+ client.events.view(objectID);
12
+ }, [client, objectID]);
13
+ }
14
+ /** useProductView as a component that renders nothing, for pages that can't call hooks, such as a
15
+ * Next.js server component. */
16
+ export function ProductView({ objectID, ...options }) {
17
+ useProductView(objectID, options);
18
+ return null;
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@findlane/browser",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Findlane browser SDK: fast, typo-tolerant public search from the browser with an index's public ID, plus React hooks and components.",
5
5
  "keywords": ["findlane", "search", "full-text search", "typo tolerance", "instant search", "ecommerce search", "browser", "react", "autocomplete"],
6
6
  "license": "MIT",