Skip to content

Commit

Permalink
feat(recent-searches): export storage and search APIs (#473)
Browse files Browse the repository at this point in the history
  • Loading branch information
francoischalifour authored Mar 3, 2021
1 parent a49a106 commit 09be485
Show file tree
Hide file tree
Showing 32 changed files with 543 additions and 172 deletions.
3 changes: 2 additions & 1 deletion .codesandbox/ci.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"/examples/js",
"/examples/react-renderer",
"/examples/query-suggestions-with-recent-searches",
"/examples/query-suggestions-with-inline-categories"
"/examples/query-suggestions-with-inline-categories",
"/examples/recently-viewed-items"
],
"node": "14"
}
Empty file.
111 changes: 111 additions & 0 deletions examples/recently-viewed-items/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/** @jsx h */
import {
autocomplete,
getAlgoliaHits,
highlightHit,
} from '@algolia/autocomplete-js';
import algoliasearch from 'algoliasearch';
import { h, Fragment } from 'preact';

import '@algolia/autocomplete-theme-classic';

import { createLocalStorageRecentlyViewedItems } from './recentlyViewedItemsPlugin';
import { ProductItem, ProductHit } from './types/ProductHit';

const appId = 'latency';
const apiKey = '6be0576ff61c053d5f9a3225e2a90f76';
const searchClient = algoliasearch(appId, apiKey);

const recentlyViewedItems = createLocalStorageRecentlyViewedItems({
key: 'RECENTLY_VIEWED',
limit: 5,
});

autocomplete({
container: '#autocomplete',
placeholder: 'Search',
openOnFocus: true,
plugins: [recentlyViewedItems],
getSources({ query }) {
if (!query) {
return [];
}

return [
{
sourceId: 'products',
getItems() {
return getAlgoliaHits<ProductItem>({
searchClient,
queries: [
{
indexName: 'instant_search',
query,
params: {
clickAnalytics: true,
attributesToSnippet: ['name:10', 'description:35'],
snippetEllipsisText: '…',
},
},
],
});
},
onSelect({ item }) {
recentlyViewedItems.data.addItem({
id: item.objectID,
label: item.name,
image: item.image,
});
},
templates: {
header() {
return (
<Fragment>
<span className="aa-SourceHeaderTitle">Products</span>
<div className="aa-SourceHeaderLine" />
</Fragment>
);
},
item({ item }) {
return <AutocompleteProductItem hit={item} />;
},
noResults() {
return (
<div className="aa-ItemContent">No products for this query.</div>
);
},
},
},
];
},
});

type ProductItemProps = {
hit: ProductHit;
};

function AutocompleteProductItem({ hit }: ProductItemProps) {
return (
<Fragment>
<div className="aa-ItemIcon aa-ItemIcon--align-top">
<img src={hit.image} alt={hit.name} width="40" height="40" />
</div>
<div className="aa-ItemContent">
<div className="aa-ItemContentTitle">
{highlightHit<ProductHit>({ hit, attribute: 'name' })}
</div>
</div>
<div className="aa-ItemActions">
<button
className="aa-ItemActionButton aa-TouchOnly aa-ActiveOnly"
type="button"
title="Select"
>
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<path d="M18.984 6.984h2.016v6h-15.188l3.609 3.609-1.406 1.406-6-6 6-6 1.406 1.406-3.609 3.609h13.172v-4.031z" />
</svg>
</button>
</div>
</Fragment>
);
}
9 changes: 9 additions & 0 deletions examples/recently-viewed-items/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { h } from 'preact';

// Parcel picks the `source` field of the monorepo packages and thus doesn't
// apply the Babel config to replace our `__DEV__` global expression.
// We therefore need to manually override it in the example app.
// See https://twitter.com/devongovett/status/1134231234605830144
(global as any).__DEV__ = process.env.NODE_ENV !== 'production';
(global as any).__TEST__ = false;
(global as any).h = h;
Binary file added examples/recently-viewed-items/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions examples/recently-viewed-items/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<link rel="shortcut icon" href="favicon.png" type="image/x-icon" />
<link rel="stylesheet" href="style.css" />

<title>Recently Viewed Items Sandbox</title>
</head>

<body>
<div class="container">
<div id="autocomplete"></div>
</div>

<script src="env.ts"></script>
<script src="app.tsx"></script>
</body>
</html>
28 changes: 28 additions & 0 deletions examples/recently-viewed-items/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@algolia/recently-viewed-items-example",
"description": "Autocomplete Recently Viewed Items Sandbox",
"version": "1.0.0-alpha.44",
"private": true,
"license": "MIT",
"main": "index.html",
"scripts": {
"build": "parcel build index.html",
"start": "parcel index.html"
},
"dependencies": {
"@algolia/autocomplete-js": "1.0.0-alpha.44",
"@algolia/autocomplete-plugin-recent-searches": "1.0.0-alpha.44",
"@algolia/autocomplete-theme-classic": "1.0.0-alpha.44",
"@algolia/client-search": "4.8.3",
"algoliasearch": "4.8.3",
"preact": "10.5.7"
},
"devDependencies": {
"parcel-bundler": "1.12.4"
},
"keywords": [
"algolia",
"autocomplete",
"javascript"
]
}
136 changes: 136 additions & 0 deletions examples/recently-viewed-items/recentlyViewedItemsPlugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/** @jsx h */
import { AutocompletePlugin, highlightHit } from '@algolia/autocomplete-js';
import {
createLocalStorageRecentSearchesPlugin,
search,
} from '@algolia/autocomplete-plugin-recent-searches';
import { h, Fragment } from 'preact';

type RecentlyViewedItem = {
id: string;
label: string;
image: string;
_highlightResult: {
label: {
value: string;
};
};
};

type CreateLocalStorageRecentlyViewedItemsParams<TItem> = {
key: string;
limit?: number;
search?(params: any): any[];
};

type RecentlyViewedItemsPluginData<TItem> = {
addItem(item: TItem): void;
removeItem(id: string): void;
getAll(query?: string): any[];
};

export function createLocalStorageRecentlyViewedItems<
TItem extends RecentlyViewedItem
>(
params: CreateLocalStorageRecentlyViewedItemsParams<TItem>
): AutocompletePlugin<TItem, RecentlyViewedItemsPluginData<TItem>> {
const {
onReset,
onSubmit,
subscribe,
...plugin
} = createLocalStorageRecentSearchesPlugin({
...params,
search(params) {
if (params.query) {
return [];
}

return search(params);
},
transformSource({ source, onRemove }) {
const transformedSource = params.transformSource
? params.transformSource({ source, onRemove })
: source;

return {
...transformedSource,
getItemUrl({ item }) {
return item.url;
},
templates: {
...transformedSource.templates,
header({ items }) {
if (items.length === 0) {
return null;
}

return (
<Fragment>
<span className="aa-SourceHeaderTitle">Recently viewed</span>
<div className="aa-SourceHeaderLine" />
</Fragment>
);
},
item({ item, createElement }) {
return (
<a className="aa-ItemLink" href={item.url}>
{item.image ? (
<div className="aa-ItemIcon">
<img src={item.image} alt={item.label} />
</div>
) : (
<div className="aa-ItemIcon aa-ItemIcon--no-border">
<svg
viewBox="0 0 24 24"
width="18"
height="18"
fill="currentColor"
>
<path d="M12.516 6.984v5.25l4.5 2.672-0.75 1.266-5.25-3.188v-6h1.5zM12 20.016q3.281 0 5.648-2.367t2.367-5.648-2.367-5.648-5.648-2.367-5.648 2.367-2.367 5.648 2.367 5.648 5.648 2.367zM12 2.016q4.125 0 7.055 2.93t2.93 7.055-2.93 7.055-7.055 2.93-7.055-2.93-2.93-7.055 2.93-7.055 7.055-2.93z" />
</svg>
</div>
)}

<div className="aa-ItemContent">
<div className="aa-ItemContentTitle">
{highlightHit<RecentlyViewedItem>({
hit: item,
attribute: 'label',
createElement,
})}
</div>
</div>
<div className="aa-ItemActions">
<button
className="aa-ItemActionButton"
title="Remove this search"
onClick={(event) => {
event.stopPropagation();
onRemove(item.id);
}}
>
<svg
viewBox="0 0 24 24"
width="18"
height="18"
fill="currentColor"
>
<path d="M18 7v13c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-10c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-13zM17 5v-1c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879h-4c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v1h-4c-0.552 0-1 0.448-1 1s0.448 1 1 1h1v13c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h10c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-13h1c0.552 0 1-0.448 1-1s-0.448-1-1-1zM9 5v-1c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h4c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707v1zM9 11v6c0 0.552 0.448 1 1 1s1-0.448 1-1v-6c0-0.552-0.448-1-1-1s-1 0.448-1 1zM13 11v6c0 0.552 0.448 1 1 1s1-0.448 1-1v-6c0-0.552-0.448-1-1-1s-1 0.448-1 1z" />
</svg>
</button>
</div>
</a>
);
},
},
};
},
});
const { getAlgoliaSearchParams, ...data } = plugin.data;

return {
...plugin,
data,
};
}
20 changes: 20 additions & 0 deletions examples/recently-viewed-items/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
* {
box-sizing: border-box;
}

body {
background-color: rgb(244, 244, 249);
color: rgb(65, 65, 65);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding: 1rem;
}

.container {
margin: 0 auto;
max-width: 640px;
width: 100%;
}
12 changes: 12 additions & 0 deletions examples/recently-viewed-items/types/ProductHit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Hit } from '@algolia/client-search';

export type ProductItem = {
name: string;
image: string;
description: string;
};

export type ProductHit = Hit<ProductItem> & {
__autocomplete_indexName: string;
__autocomplete_queryID: string;
};
2 changes: 1 addition & 1 deletion packages/autocomplete-core/src/types/AutocompletePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { OnSelectParams, OnActiveParams } from './AutocompleteSource';

type PluginSubscriber<TParams> = (params: TParams) => void;

interface PluginSubscribeParams<TItem extends BaseItem>
export interface PluginSubscribeParams<TItem extends BaseItem>
extends AutocompleteScopeApi<TItem> {
onSelect(fn: PluginSubscriber<OnSelectParams<TItem>>): void;
onActive(fn: PluginSubscriber<OnActiveParams<TItem>>): void;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Highlighted, HistoryItem } from './types';

type HighlightParams<TItem> = {
item: TItem;
query: string;
};

export function addHighlightedAttribute<TItem extends HistoryItem>({
item,
query,
}: HighlightParams<TItem>): Highlighted<TItem> {
return {
...item,
_highlightResult: {
label: {
value: query
? item.label.replace(
new RegExp(query.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'), 'gi'),
(match) => {
return `__aa-highlight__${match}__/aa-highlight__`;
}
)
: item.label,
},
},
};
}
Loading

0 comments on commit 09be485

Please sign in to comment.